Last active
April 15, 2019 22:17
-
-
Save mateo-leal/ce057eb7de80e04be46b6becf716cfd5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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