package com.im.imjutil.dao.jpa;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Persistence;
import javax.persistence.TemporalType;
import com.im.imjutil.config.Configurator;
import com.im.imjutil.dao.DAO;
import com.im.imjutil.dao.Transaction;
import com.im.imjutil.exception.JPAException;
import com.im.imjutil.exception.PersistenceException;
import com.im.imjutil.exception.ValidationException;
import com.im.imjutil.logging.Logger;
import com.im.imjutil.query.Hints;
import com.im.imjutil.query.NativeQuery;
import com.im.imjutil.query.Query;
import com.im.imjutil.query.QueryAdapter;
import com.im.imjutil.util.Filter;
import com.im.imjutil.util.Pair;
import com.im.imjutil.validation.Convert;
import com.im.imjutil.validation.Validator;
/**
* Classe que implementa um DAO JAP generico para persistencia basica
* de qualquer modelo JPA que implemente a interface {@link DAO}.
* Configure no arquivo {@code /META-INF/config.property} o nome
* da unidade de persistencia sob a tag: {@code jpa.persistence.unit}
* <br>
* Veja a sintaxe do EJB-QL
* <a href='http://java.sun.com/j2ee/tutorial/1_3-fcs/doc/EJBQL5.html'>
* AQUI
* </a>
*
* @author Felipe Zappala
*
* @param <T> Tipo do modelo a ser persistido.
*/
public class JPADAO<T> implements DAO<T> {
/** Mapa com as configuracoes da unidade de persistencia. */
private Map<String, String> unitProperties;
/** Instancia da classe do tipo especifico do DAO. */
protected final Class<T> clazz;
/** Entity Manager Factory padrao do sistema. */
protected EntityManagerFactory factory;
/** Entity Manager padrao do sistema. */
protected EntityManager manager;
/** Nome da unidade de persistencia do JPA*/
public static String PERSISTENCE_UNIT;
/** Mensagem: Erro ao executar operacao. */
public static final String MSG_ERROR_OPT;
/** Determina se o DAO deve gerenciar automaticamente as transacoes das operacoes. */
private boolean autoManageTransactions = true;
static {
PERSISTENCE_UNIT = Configurator.getProperty("jpa.persistence.unit");
MSG_ERROR_OPT = "Erro ao executar operacao: ";
}
/**
* Constroi um DAO genrico passando a instancia da classe do tipo deste.
*
* @param clazz Instancia da classe do tipo especifico do DAO.
*/
public JPADAO(Class<T> clazz) {
this(clazz, null, null);
}
/**
* Constroi um DAO genrico passando a instancia da classe do tipo deste.
*
* @param clazz Instancia da classe do tipo especifico do DAO.
* @param em Instancia do gerenciador de entidades alvo.
*/
public JPADAO(Class<T> clazz, EntityManager em) {
this(clazz, em, null);
}
/**
* Constroi um DAO genrico passando a instancia da classe do tipo deste.
*
* @param clazz Instancia da classe do tipo especifico do DAO.
* @param unitProperties Propriedades da unidade de persistencia.
*/
public JPADAO(Class<T> clazz, Map<String, String> unitProperties) {
this(clazz, null, unitProperties);
}
/**
* Constroi um DAO genrico passando a instancia da classe do tipo deste.
*
* @param clazz Instancia da classe do tipo especifico do DAO.
* @param em Instancia do gerenciador de entidades alvo.
* @param unitProperties Propriedades da unidade de persistencia.
*/
public JPADAO(Class<T> clazz, EntityManager em,
Map<String, String> unitProperties) {
if (unitProperties != null) {
this.unitProperties = unitProperties;
}
if (clazz == null) {
throw new ValidationException("Parametro clazz e nulo");
}
this.clazz = clazz;
if (em == null) {
// Inicializa a unidade de persistencia
initializePersistenceUnit();
// Inicializa o Entity Manager da classe atual.
this.manager = factory.createEntityManager();
} else {
// Utiliza o Entity Manager passado
this.manager = em;
}
}
@Override
public T add(T type) throws PersistenceException {
T merged = type;
try {
beginTransaction();
merged = manager.merge(type);
manager.flush();
commitTransaction();
} catch (Exception e) {
rollbackTransaction();
String error = Convert.toString(MSG_ERROR_OPT, "add: ", type);
Logger.error(error, e);
throw new JPAException(error, e);
}
return merged;
}
@Override
public void addAll(List<T> types) throws PersistenceException {
List<T> mergeds = new ArrayList<T>(types.size());
try {
beginTransaction();
for (T type : types) {
mergeds.add(manager.merge(type));
}
manager.flush();
commitTransaction();
types.clear();
types.addAll(mergeds);
} catch (Exception e) {
rollbackTransaction();
String error = Convert.toString(MSG_ERROR_OPT, "addAll");
Logger.error(error, e);
throw new JPAException(error, e);
}
}
@Override
public T find(Filter filter) throws PersistenceException {
try {
if (filter == null) {
throw new ValidationException("O filtro passado e nulo.");
}
return this.execute(getQuery(filter), filter);
} catch (Exception e) {
String error = Convert.toString(MSG_ERROR_OPT, "filter: ", filter);
Logger.error(error, e);
throw new JPAException(error, e);
}
}
@Override
public List<T> findAll(Filter filter) throws PersistenceException {
try {
if (filter == null) {
throw new ValidationException("O filtro passado e nulo.");
}
return this.executeAll(getQuery(filter), filter);
} catch (Exception e) {
String error = Convert.toString(MSG_ERROR_OPT, "filter: ", filter);
Logger.error(error, e);
throw new JPAException(error, e);
}
}
@Override
public T get(Object id) throws PersistenceException {
try {
return manager.find(clazz, id);
} catch (Exception e) {
String error = Convert.toString(MSG_ERROR_OPT, "get: ", id);
Logger.error(error, e);
throw new JPAException(error, e);
}
}
@Override
@SuppressWarnings({ "cast", "unchecked" })
public List<T> getAll() throws PersistenceException {
try {
if (hasQueryFindAll()) {
String sql = Convert.toString(clazz.getSimpleName(),".findAll");
return (List<T>) manager.createNamedQuery(sql).getResultList();
}
return executeAll(new QueryAdapter(Convert.toString(
"SELECT t FROM ", clazz.getSimpleName(), " AS t"
)));
} catch (Exception e) {
String error = Convert.toString(MSG_ERROR_OPT, "getAll");
Logger.error(error, e);
throw new JPAException(error, e);
}
}
@Override
public void remove(T type) throws PersistenceException {
try {
beginTransaction();
manager.remove(manager.merge(type));
manager.flush();
commitTransaction();
} catch (Exception e) {
rollbackTransaction();
String error = Convert.toString(MSG_ERROR_OPT, "remove: ", type);
Logger.error(error, e);
throw new JPAException(error, e);
}
}
@Override
public void removeAll(List<T> types) throws PersistenceException {
try {
beginTransaction();
for (T type : types) {
manager.remove(manager.merge(type));
}
manager.flush();
commitTransaction();
} catch (Exception e) {
rollbackTransaction();
String error = Convert.toString(MSG_ERROR_OPT, "removeAll");
Logger.error(error, e);
throw new JPAException(error, e);
}
}
@Override
@SuppressWarnings({ "unchecked", "cast" })
public <R> R execute(Query query, Filter... filters)
throws PersistenceException {
try {
beginTransaction();
// Cria a consulta
String queryString = query.createQuery(filters);
javax.persistence.Query queryJPA;
if (query instanceof NativeQuery) {
NativeQuery nativeQuery = (NativeQuery) query;
Class<?> target = getTargetClass(nativeQuery);
if (target != null)
queryJPA = manager.createNativeQuery(queryString, target);
else
queryJPA = manager.createNativeQuery(queryString);
} else {
queryJPA = manager.createQuery(queryString);
}
Filter filter = new Filter().addFilters(filters);
// Associa os valores
for (Pair<String, Object> pair : filter) {
setQuery(queryJPA, pair);
}
setQueryHints(queryJPA, query);
// Se for uma consulta de 'delete' ou 'updade',
// retorna o numero de linhas afetadas.
// Se for um 'select', retorn o seu objeto resultante.
if (!queryString.toLowerCase().startsWith("select")) {
R ret = (R) Integer.valueOf(queryJPA.executeUpdate());
commitTransaction();
return ret;
}
// Caso for uma consulta 'select', obtem o primeiro resultado
// via 'getResultList' pra evitar o erro 'NonUniqueResultException'
// configurando o limite dos resultados para obter o primeiro
queryJPA.setFirstResult(0);
queryJPA.setMaxResults(1);
List<R> results = (List<R>) queryJPA.getResultList();
commitTransaction();
if (!Validator.isEmpty(results)) {
return results.get(0);
}
return null;
} catch (Exception e) {
rollbackTransaction();
String error = Convert.toString(MSG_ERROR_OPT, "execute");
Logger.error(error, e);
throw new JPAException(error, e);
}
}
private Class<?> getTargetClass(NativeQuery nativeQuery) {
if (nativeQuery.getTarget() != null) {
if (nativeQuery.getTarget() == Void.class) {
return null;
}
return nativeQuery.getTarget();
}
return this.clazz;
}
private void setQueryHints(javax.persistence.Query queryJPA, Query query) {
if (query instanceof Hints) {
Hints hints = (Hints) query;
Properties props = hints.getHints();
if (!props.isEmpty()) {
for (Map.Entry<?, ?> pair : props.entrySet()) {
queryJPA.setHint((String)pair.getKey(), pair.getValue());
}
}
}
}
@Override
@SuppressWarnings({ "cast", "unchecked" })
public <R> List<R> executeAll(Query query, Filter... filters)
throws PersistenceException {
try {
beginTransaction();
// Cria a consulta
String queryString = query.createQuery(filters);
javax.persistence.Query queryJPA;
if (query instanceof NativeQuery) {
NativeQuery nativeQuery = (NativeQuery) query;
Class<?> target = (nativeQuery.getTarget() != null) ? nativeQuery.getTarget() : this.clazz;
queryJPA = manager.createNativeQuery(queryString, target);
} else {
queryJPA = manager.createQuery(queryString);
}
Filter filter = new Filter().addFilters(filters);
// Associa os valores
for (Pair<String, Object> pair : filter) {
setQuery(queryJPA, pair);
}
// Configura os limites
if (filter.isLimited()) {
queryJPA.setFirstResult(filter.getLimits().getFirst());
queryJPA.setMaxResults(filter.getLimits().getLast());
}
setQueryHints(queryJPA, query);
// Executa a consulta
List<R> results = (List<R>) queryJPA.getResultList();
commitTransaction();
return results;
} catch (Exception e) {
String error = Convert.toString(MSG_ERROR_OPT, "executeAll");
Logger.error(error, e);
throw new JPAException(error, e);
}
}
private Set<String> getAvailableFields() {
Set<String> availableFields = new HashSet<String>();
for (Field field : clazz.getDeclaredFields()) {
availableFields.add(field.getName());
}
Class<?> superClass = clazz.getSuperclass();
while (!superClass.equals(Object.class)) {
for (Field field : superClass.getDeclaredFields()) {
availableFields.add(field.getName());
}
superClass = superClass.getSuperclass();
}
return availableFields;
}
/**
* Obtem a string da consulta do JPA.
*
* @param filter Filtros da consulta.
* @return String da consulta EJB-QL.
*/
private Query getQuery(Filter filter) {
String queryString = null;
if (!filter.isQueriable()) {
// Prepara a consulta
StringBuilder sql = new StringBuilder();
sql.append("SELECT o FROM ");
sql.append(clazz.getSimpleName());
sql.append(" AS o");
// Obtem os filtros disponiveis
Set<String> availableFields = getAvailableFields();
// Associa os filtros
if (!filter.isEmpty()) {
sql.append(" WHERE ");
for (Pair<String, Object> pair : filter) {
String key = pair.getFirst().replaceAll("\\!\\%", "");
if (!availableFields.contains(key)) {
throw new ValidationException(Convert.toString(
"Campo invalido para filtro da classe ",
clazz.getSimpleName(), ": ", key));
}
sql.append("o.").append(key);
sql.append((pair.getLast() instanceof String)
? " LIKE " : " = " );
sql.append(":").append(key);
sql.append(filter.isExclusive() ? " AND " : " OR ");
}
// Remove o ultimo ' AND ' ou ' OR ''
sql.setLength(sql.length() -5);
}
setOrderBy(filter, sql);
queryString = sql.toString();
} else {
// Cria a string da consulta usando a query do filtro.
queryString = filter.getQuery().createQuery(filter);
}
return new QueryAdapter(queryString);
}
/**
* Configura a ordenacao na consulta.
*
* @param filter Filtro com ordenacao.
* @param sql Consulta a ser ordenada.
*/
private void setOrderBy(Filter filter, StringBuilder sql) {
if (filter.isOrdened()) {
sql.append(" ORDER BY ");
for (Pair<String, Boolean> order : filter.getOrders()) {
sql.append("o.").append(order.getFirst());
sql.append(order.getLast() ? " ASC, " : " DESC, ");
}
sql.setLength(sql.length() -2);
}
}
/**
* Configura os dados do pair na consulta.
*
* @param queryJPA Consulta JPA
* @param pair Par de dados.
*/
private void setQuery(javax.persistence.Query queryJPA, Pair<String, Object> pair) {
String key = pair.getFirst();
boolean isIntKey = Validator.isNumber(key);
int ikey = (isIntKey) ? Convert.toInteger(key) : 0;
if (pair.getLast() instanceof String) {
String value = (String) pair.getLast();
if (key.startsWith("!%") && key.endsWith("!%")) {
key = key.substring(2, key.length()-2);
} else if (key.startsWith("!%")) {
key = key.substring(2, key.length());
value = Convert.toString(value, "%");
} else if (key.endsWith("!%")) {
key = key.substring(0, key.length()-2);
value = Convert.toString("%", value);
} else {
value = Convert.toString("%", value, "%");
}
if (isIntKey)
queryJPA.setParameter(ikey, value);
else
queryJPA.setParameter(key, value);
} else if (pair.getLast() instanceof Calendar) {
if (isIntKey)
queryJPA.setParameter(ikey, (Calendar)
pair.getLast(), TemporalType.TIMESTAMP);
else
queryJPA.setParameter(pair.getFirst(), (Calendar)
pair.getLast(), TemporalType.TIMESTAMP);
} else if (pair.getLast() instanceof java.sql.Date) {
if (isIntKey)
queryJPA.setParameter(ikey, (Date)
pair.getLast(), TemporalType.DATE);
else
queryJPA.setParameter(pair.getFirst(), (Date)
pair.getLast(), TemporalType.DATE);
} else if (pair.getLast() instanceof java.sql.Time) {
if (isIntKey)
queryJPA.setParameter(ikey, (Date)
pair.getLast(), TemporalType.TIME);
else
queryJPA.setParameter(pair.getFirst(), (Date)
pair.getLast(), TemporalType.TIME);
} else if (pair.getLast() instanceof java.sql.Timestamp) {
if (isIntKey)
queryJPA.setParameter(ikey, (Date)
pair.getLast(), TemporalType.TIMESTAMP);
else
queryJPA.setParameter(pair.getFirst(), (Date)
pair.getLast(), TemporalType.TIMESTAMP);
} else if (pair.getLast() instanceof Date) {
if (isIntKey)
queryJPA.setParameter(ikey, (Date)
pair.getLast(), TemporalType.TIMESTAMP);
else
queryJPA.setParameter(pair.getFirst(), (Date)
pair.getLast(), TemporalType.TIMESTAMP);
} else {
if (isIntKey)
queryJPA.setParameter(ikey, pair.getLast());
else
queryJPA.setParameter(pair.getFirst(), pair.getLast());
}
}
/**
* Inicializa a unidade de persistencia padrao do sistema.
*/
private void initializePersistenceUnit() {
try {
if (!Validator.isValid(PERSISTENCE_UNIT)) {
throw new ValidationException(
"A propriedade 'jpa.persistence.unit' esta nula.");
}
// Inicializa a fabrica de gerenciadores de entidade.
if (this.unitProperties != null) {
factory = Persistence.createEntityManagerFactory(
PERSISTENCE_UNIT, this.unitProperties);
} else {
factory = Persistence.createEntityManagerFactory(
PERSISTENCE_UNIT);
}
if (factory == null) {
throw new PersistenceException(Convert.toString(
"A EntityManagerFactory para a unidade de persistencia",
" '", PERSISTENCE_UNIT, "' e nula."));
}
} catch (Exception e) {
throw new PersistenceException(Convert.toString(
"Erro ao obter unidade de persistencia JPA: ",
PERSISTENCE_UNIT), e);
}
}
/**
* Verifica se a classe alvo do DAO possui uma consulta nomeada no padrao:
* {@code ClassName.findAll}
*
* @return Verdadeiro quando a classe contiver a consulta.
*/
private boolean hasQueryFindAll() {
String name = Convert.toString(clazz.getSimpleName(), ".findAll");
NamedQueries nq = clazz.getAnnotation(NamedQueries.class);
if (nq != null) {
for (NamedQuery q : nq.value()) {
if (q.name().equals(name)){
return true;
}
}
}
return false;
}
/**
* Finaliza o Entity Manager da classe atual.
*/
@Override
protected void finalize() throws Throwable {
if (this.manager != null) {
this.manager.close();
this.manager = null;
}
if (this.factory != null) {
this.factory.close();
this.factory = null;
}
}
@Override
public String toString() {
return Convert.toString(this.getClass().getSimpleName(), "{",
"Class: ", this.clazz.getName(), ", ",
"PersistenceUnit: ", PERSISTENCE_UNIT, ", ",
"EntityManager: ", manager.getClass().getName(), ", ",
"EntityManagerFactory: ", factory.getClass().getName(),
"}");
}
/**
* Retorna a instancia do gerenciador de entidades da DAO JPA corrente
*
* @return Retorna um objeto de {@link EntityManager}
*/
public EntityManager getEntityManager() {
return manager;
}
@Override
public Transaction transaction() {
autoManageTransactions = false;
return new JPATransactionAdapter(this.manager.getTransaction());
}
/**
* Gerencia o rollback das transacoes automaticamente
*/
private void rollbackTransaction() {
if (autoManageTransactions)
try{manager.getTransaction().rollback();}catch(Exception x) {/**/}
}
/**
* Gerencia o commit das transacoes automaticamente
*/
private void commitTransaction() {
if (autoManageTransactions)
manager.getTransaction().commit();
}
/**
* Gerencia o begin das transacoes automaticamente
*/
private void beginTransaction() {
if (autoManageTransactions)
manager.getTransaction().begin();
}
}