Skip to content

Instantly share code, notes, and snippets.

@mateo-leal
Last active April 15, 2019 22:17
Show Gist options
  • Select an option

  • Save mateo-leal/ce057eb7de80e04be46b6becf716cfd5 to your computer and use it in GitHub Desktop.

Select an option

Save mateo-leal/ce057eb7de80e04be46b6becf716cfd5 to your computer and use it in GitHub Desktop.
import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.LockTimeoutException;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceException;
import javax.persistence.PessimisticLockException;
import javax.persistence.Query;
import javax.persistence.QueryTimeoutException;
import javax.persistence.TransactionRequiredException;
import org.eclipse.persistence.config.QueryHints;
import org.eclipse.persistence.config.ResultType;
import org.eclipse.persistence.internal.helper.DatabaseField;
import com.google.common.base.CaseFormat;
import com.mateolegi.Rostrum.numbercast.DoubleCast;
import com.mateolegi.Rostrum.numbercast.FloatCast;
import com.mateolegi.Rostrum.numbercast.IntegerCast;
import com.mateolegi.Rostrum.numbercast.LongCast;
import com.mateolegi.Rostrum.numbercast.NumberCast;
/**
* Esta clase tiene el objetivo de transformar los objetos genéricos retornados por
* las consultas nativas de JPA en un POJO con los atributos requeridos <br>
* Creado el 1/12/2018 a las 12:05:12 p.m. <br>
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @version 3.1.0
*/
public class EntityFactory {
/** Consulta crea con los parámetros */
private final Query query;
/** Mapa con los tipos de números compatibles con EntityFactory */
private static final Map<Class<?>, NumberCast> numberCast = new HashMap<>();
private static final Logger LOGGER = Logger.getLogger(EntityFactory.class.getSimpleName());
static {
numberCast.put(Long.class, new LongCast());
numberCast.put(Integer.class, new IntegerCast());
numberCast.put(Double.class, new DoubleCast());
numberCast.put(Float.class, new FloatCast());
}
/**
* Crea una instancia de EntityFactory con una consulta inmodificable.
* @param query consulta con los parámetros
* @since 3.0.0
*/
public EntityFactory(final Query query) {
Objects.requireNonNull(query, "El query no puede ser nulo");
this.query = query;
}
/**
* Ejecuta una consulta y mapea el resultado en un DTO.
* Retorna un único resultado, en caso que la consulta devuelva más
* de un registro o ningún registro se lanzan las excepciones de {@code Query} <br>
* Creado el 24/01/2019 a las 11:42:00 a.m. <br>
* @param clazz clase a la que se quiere transformar el resultado de la consulta
* @return instancia de la clase
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 3.0.0
* @throws NonUniqueResultException si hay más de un resultado
* @throws IllegalStateException si se llama para una instrucción UPDATE o
* DELETE del lenguaje de consulta de persistencia de Java
* @throws QueryTimeoutException si la ejecución de la consulta supera el
* valor del tiempo de espera de la consulta y solo se revierte la
* instrucción
* @throws TransactionRequiredException si se ha establecido un modo de
* bloqueo distinto de {@code NONE} y no hay transacción o el
* contexto de persistencia no se ha unido a la transacción
* @throws PessimisticLockException si el bloqueo pesimista falla y
* la transacción se revierte
* @throws LockTimeoutException si el bloqueo pesimista falla y solo
* se revertirá la instrucción
* @throws PersistenceException si la ejecución de la consulta supera el
* valor de tiempo de espera de la consulta y la transacción se retrotrae
*/
public <T> Optional<T> reflect(final Class<T> clazz) {
return Optional.ofNullable(transform(query, clazz));
}
/**
* Ejecuta una consulta y mapea el resultado en un DTO. <br>
* Creado el 12/12/2018 a las 3:30:25 p.m. <br>
* @param clazz clase a la que se quiere transformar el resultado de la consulta
* @return instancia de la clase
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 3.0.0
* @throws IllegalStateException si se llama para una instrucción UPDATE o
* DELETE del lenguaje de consulta de persistencia de Java
* @throws QueryTimeoutException si la ejecución de la consulta supera el
* valor del tiempo de espera de la consulta y solo se revierte la instrucción
* @throws TransactionRequiredException si se ha establecido un modo de
* bloqueo distinto de {@code NONE} y no hay transacción o el
* contexto de persistencia no se ha unido a la transacción
* @throws PessimisticLockException si el bloqueo pesimista falla y
* la transacción se revierte
* @throws LockTimeoutException si el bloqueo pesimista falla y solo
* se revertirá la instrucción
* @throws PersistenceException si la ejecución de la consulta supera el
* valor de tiempo de espera de la consulta y la transacción se retrotrae
*/
public <T> List<T> reflectList(final Class<T> clazz) {
return transformList(query, clazz);
}
/**
* Ejecuta una consulta y mapea el resultado en un DTO.
* Retorna un único resultado, en caso que la consulta devuelva más
* de un registro o ningún registro se lanzan las excepciones de {@code Query} <br>
* Creado el 24/01/2019 a las 11:42:00 a.m. <br>
* @param query después de asignarle los parámetros
* @param clazz clase a la que se quiere transformar el resultado de la consulta
* @return instancia de la clase
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 2.1.0
* @throws IllegalStateException si se llama para una instrucción UPDATE o
* DELETE del lenguaje de consulta de persistencia de Java
* @throws QueryTimeoutException si la ejecución de la consulta supera el
* valor del tiempo de espera de la consulta y solo se revierte la
* instrucción
* @throws TransactionRequiredException si se ha establecido un modo de
* bloqueo distinto de {@code NONE} y no hay transacción o el
* contexto de persistencia no se ha unido a la transacción
* @throws PessimisticLockException si el bloqueo pesimista falla y
* la transacción se revierte
* @throws LockTimeoutException si el bloqueo pesimista falla y solo
* se revertirá la instrucción
* @throws PersistenceException si la ejecución de la consulta supera el
* valor de tiempo de espera de la consulta y la transacción se retrotrae
*/
@SuppressWarnings("unchecked")
private static <T> T transform(final Query query, final Class<T> clazz) {
try {
query.setHint(QueryHints.RESULT_TYPE, ResultType.Map);
Map<DatabaseField, ?> map = (Map<DatabaseField, ?>) query.getSingleResult();
return mapToDTO(map, clazz);
} catch (NoResultException e) {
return null;
}
}
/**
* Ejecuta una consulta y mapea el resultado en un DTO. <br>
* Creado el 12/12/2018 a las 3:30:25 p. m. <br>
* @param query después de asignarle los parámetros
* @param clazz clase a la que se quiere transformar el resultado de la consulta
* @return instancia de la clase
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 2.0.0
* @throws IllegalStateException si se llama para una instrucción UPDATE o
* DELETE del lenguaje de consulta de persistencia de Java
* @throws QueryTimeoutException si la ejecución de la consulta supera el
* valor del tiempo de espera de la consulta y solo se revierte la instrucción
* @throws TransactionRequiredException si se ha establecido un modo de
* bloqueo distinto de {@code NONE} y no hay transacción o el
* contexto de persistencia no se ha unido a la transacción
* @throws PessimisticLockException si el bloqueo pesimista falla y
* la transacción se revierte
* @throws LockTimeoutException si el bloqueo pesimista falla y solo
* se revertirá la instrucción
* @throws PersistenceException si la ejecución de la consulta supera el
* valor de tiempo de espera de la consulta y la transacción se retrotrae
*/
@SuppressWarnings("unchecked")
private static <T> List<T> transformList(final Query query, final Class<T> clazz) {
List<T> response = new ArrayList<>();
query.setHint(QueryHints.RESULT_TYPE, ResultType.Map);
List<Map<DatabaseField, ?>> res = query.getResultList();
res.forEach(map -> response.add(mapToDTO(map, clazz)));
return response;
}
/**
* Transforma un mapa retorando por el Query en una instancia de
* la clase dada por parámetro <br>
* Creado el 12/12/2018 a las 3:53:57 p.m. <br>
* @param map mapa con el resultado de la consulta
* @param clazz clase a la que se va a mapear
* @return instancia con los valores del mapa
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 2.0.0
*/
private static <T> T mapToDTO(Map<DatabaseField, ?> map, final Class<T> clazz) {
try {
if (map.size() == 1 && isSimpleObject(clazz)) {
return mapSingleObject(map.values().iterator().next(), clazz);
}
T instance = clazz.newInstance();
List<Field> fields = Arrays.asList(clazz.getDeclaredFields());
map.entrySet().stream()
.filter(field -> obtenerCampo(field.getKey().getName(), fields) != null)
.forEach(i -> {
Field field = obtenerCampo(i.getKey().getName(), fields);
setValue(field, i.getValue(), instance);
});
return instance;
} catch (InstantiationException | IllegalAccessException e) {
LOGGER.log(Level.SEVERE, "Error creando una instancia de " + clazz.getName(), e);
}
return null;
}
private static boolean isSimpleObject(Class<?> clazz) {
return clazz == String.class || Number.class.isAssignableFrom(clazz)
|| clazz == Date.class || clazz == LocalDateTime.class
|| clazz == LocalDate.class;
}
/**
* Moldea objetos sencillos de un mapa con una sola llave a objetos como
* String, Long, Integer, Double y Float. <br>
* Creado el 15/04/2019 a las 12:09:06 p.m. <br>
* @param o objeto
* @param clazz clase final
* @return objeto instancia de la clase final
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 3.1.0
*/
@SuppressWarnings("unchecked")
private static <T> T mapSingleObject(Object o, Class<T> clazz) {
return (T) castParam(o, clazz);
}
/**
* Asigna el valor al atributo de la instancia <br>
* Creado el 12/12/2018 a las 3:56:49 p.m. <br>
* @param field
* @param o
* @param instance
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 2.0.0
*/
private static <T> void setValue(Field field, Object o, T instance) {
try {
boolean isAccessible = field.isAccessible();
field.setAccessible(true);
field.set(instance, castParam(o, field.getType()));
field.setAccessible(isAccessible);
} catch (IllegalArgumentException | IllegalAccessException e) {
LOGGER.log(Level.WARNING, "Error seteando valor " + o + " en el campo " + field.getName(), e);
}
}
/**
* Busca coincidencias entre el nombre de la columna y el nombre del atributo.
* Independiente de si está en {@code camelCase} o en {@code SNAKE_CASE} <br>
* Creado el 13/12/2018 a las 9:05:15 a. m. <br>
* @param databaseField nombre de la columna
* @param fields lista con los campos de la clase
* @return el campo de la clase que coincide, de lo contrario {@code null}
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 2.0.0
*/
private static Field obtenerCampo(String databaseField, List<Field> fields) {
return fields.stream().filter(field -> field.getName().equalsIgnoreCase(calcularNombreCampo(databaseField)))
.findFirst()
.orElse(null);
}
/**
* Valida si el nombre de la columna está en {@code SNAKE_CASE} y la transforma a
* {@code camelCase} y valida si tienen el mismo valor <br>
* Creado el 13/12/2018 a las 9:07:44 a. m. <br>
* @param queryName nombre de la columna de la consulta
* @return nombre nombre de la columna transformado
* @since 2.0.0
*/
private static String calcularNombreCampo(final String queryName) {
if (queryName.contains("_")) {
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, queryName);
}
return queryName;
}
/**
* Moldea un objeto a la clase dada <br>
* Creado el 13/12/2018 a las 9:10:05 a. m. <br>
* @param o objeto
* @param c clase a moldear
* @return instancia de la clase con el valor del objeto
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
* @since 2.0.0
*/
private static Object castParam(Object o, Class<?> c) {
try {
if (o == null) {
return null;
} else if (Number.class.isAssignableFrom(c)) {
// Casos especiales para moldear los números
return castNumber(o, c);
} else if (c == Date.class && o.getClass() == Timestamp.class) {
Timestamp timestamp = (Timestamp) o;
return new Date(timestamp.getTime());
} else if (c == LocalDateTime.class && o.getClass() == Timestamp.class) {
Timestamp timestamp = (Timestamp) o;
return timestamp.toLocalDateTime();
} else if (c == LocalDate.class && o.getClass() == Timestamp.class) {
Timestamp timestamp = (Timestamp) o;
return timestamp.toLocalDateTime().toLocalDate();
} else if (c == String.class && !(o instanceof String)) {
return o.toString();
} else {
return c.cast(o);
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Ocurrió un error moldeando el objeto con valor "
+ o + " a la clase " + c.getName(), e);
throw e;
}
}
/**
* Moldea los números a la clase enviada por parámetro <br>
* Creado el 1/12/2018 a las 12:23:54 p. m. <br>
* @param o objeto con el número
* @param clazz clase a la que debe ser moldeada
* @return número moldeado
* @author <a href="https://mateolegi.github.io">Mateo Leal</a>
*/
private static Number castNumber(Object o, Class<?> clazz) {
return numberCast.get(clazz).cast(o);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment