All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.omnifaces.persistence.JPA Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2021 OmniFaces
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.omnifaces.persistence;

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static org.omnifaces.persistence.Database.POSTGRESQL;
import static org.omnifaces.persistence.Provider.HIBERNATE;
import static org.omnifaces.utils.stream.Collectors.toMap;
import static org.omnifaces.utils.stream.Streams.stream;

import java.lang.reflect.AnnotatedElement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.naming.InitialContext;

import jakarta.ejb.SessionContext;
import jakarta.enterprise.inject.Typed;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.NoResultException;
import jakarta.persistence.NonUniqueResultException;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.ValidationMode;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.CollectionAttribute;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.ListAttribute;
import jakarta.persistence.metamodel.MapAttribute;
import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.SetAttribute;
import jakarta.persistence.metamodel.SingularAttribute;

import org.omnifaces.persistence.criteria.Numeric;
import org.omnifaces.persistence.service.BaseEntityService;

/**
 * JPA utilities.
 */
@Typed
public final class JPA {

    // Public constants -------------------------------------------------------------------------------------------------------------------

    public static final String QUERY_HINT_LOAD_GRAPH = "jakarta.persistence.loadgraph";
    public static final String QUERY_HINT_FETCH_GRAPH = "jakarta.persistence.fetchgraph";
    public static final String QUERY_HINT_CACHE_STORE_MODE = "jakarta.persistence.cache.storeMode"; // USE | BYPASS | REFRESH
    public static final String QUERY_HINT_CACHE_RETRIEVE_MODE = "jakarta.persistence.cache.retrieveMode"; // USE | BYPASS
    public static final String PROPERTY_VALIDATION_MODE = "jakarta.persistence.validation.mode"; // AUTO | CALLBACK | NONE


    // Constructors -----------------------------------------------------------------------------------------------------------------------

    private JPA() {
        throw new AssertionError();
    }


    // Configuration utils ----------------------------------------------------------------------------------------------------------------

    /**
     * Returns the currently configured bean validation mode for given entity manager.
     * This consults the {@value #PROPERTY_VALIDATION_MODE} property in persistence.xml.
     * @param entityManager The involved entity manager.
     * @return The currently configured bean validation mode.
     */
    public static ValidationMode getValidationMode(EntityManager entityManager) {
        var validationMode = entityManager.getEntityManagerFactory().getProperties().get(PROPERTY_VALIDATION_MODE);
        return validationMode != null ? ValidationMode.valueOf(validationMode.toString().toUpperCase()) : ValidationMode.AUTO;
    }


    // Query utils ------------------------------------------------------------------------------------------------------------------------

    /**
     * Returns single result of given typed query as {@link Optional}.
     * @param  The generic result type.
     * @param typedQuery The involved typed query.
     * @return Single result of given typed query as {@link Optional}.
     * @throws NonUniqueResultException When there is no unique result.
     */
    public static  Optional getOptionalSingleResult(TypedQuery typedQuery) {
        return ofNullable(getSingleResultOrNull(typedQuery));
    }

    /**
     * Returns single result of given query as {@link Optional}.
     * @param  The expected result type.
     * @param query The involved query.
     * @return Single result of given query as {@link Optional}.
     * @throws NonUniqueResultException When there is no unique result.
     * @throws ClassCastException When T is of wrong type.
     */
    public static  Optional getOptionalSingleResult(Query query) {
        return ofNullable(getSingleResultOrNull(query));
    }

    /**
     * Returns single result of given typed query, or null if there is none.
     * @param  The generic result type.
     * @param typedQuery The involved typed query.
     * @return Single result of given typed query, or null if there is none.
     * @throws NonUniqueResultException When there is no unique result.
     */
    public static  T getSingleResultOrNull(TypedQuery typedQuery) {
        try {
            return typedQuery.getSingleResult();
        }
        catch (NoResultException e) {
            return null;
        }
    }

    /**
     * Returns single result of given query, or null if there is none.
     * @param  The expected result type.
     * @param query The involved query.
     * @return Single result of given query, or null if there is none.
     * @throws NonUniqueResultException When there is no unique result.
     * @throws ClassCastException When T is of wrong type.
     */
    @SuppressWarnings("unchecked")
    public static  T getSingleResultOrNull(Query query) {
        try {
            return (T) query.getSingleResult();
        }
        catch (NoResultException e) {
            return null;
        }
    }

    /**
     * Returns first result of given typed query as {@link Optional}.
     * The difference with {@link #getOptionalSingleResult(TypedQuery)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches.
     * @param  The generic result type.
     * @param typedQuery The involved typed query.
     * @return First result of given typed query as {@link Optional}.
     */
    public static  Optional getOptionalFirstResult(TypedQuery typedQuery) {
        typedQuery.setMaxResults(1);
        return typedQuery.getResultList().stream().findFirst();
    }

    /**
     * Returns first result of given query as {@link Optional}.
     * The difference with {@link #getOptionalSingleResult(Query)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches.
     * @param  The expected result type.
     * @param query The involved query.
     * @return First result of given query as {@link Optional}.
     * @throws ClassCastException When T is of wrong type.
     */
    @SuppressWarnings("unchecked")
    public static  Optional getOptionalFirstResult(Query query) {
        query.setMaxResults(1);
        return query.getResultList().stream().findFirst();
    }

    /**
     * Returns first result of given typed query, or null if there is none.
     * The difference with {@link #getSingleResultOrNull(TypedQuery)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches.
     * @param  The generic result type.
     * @param typedQuery The involved typed query.
     * @return First result of given typed query, or null if there is none.
     */
    public static  T getFirstResultOrNull(TypedQuery typedQuery) {
        return getOptionalFirstResult(typedQuery).orElse(null);
    }

    /**
     * Returns first result of given query, or null if there is none.
     * The difference with {@link #getSingleResultOrNull(Query)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches.
     * @param  The expected result type.
     * @param query The involved query.
     * @return First result of given query, or null if there is none.
     * @throws ClassCastException When T is of wrong type.
     */
    @SuppressWarnings("unchecked")
    public static  T getFirstResultOrNull(Query query) {
        return (T) getOptionalFirstResult(query).orElse(null);
    }

    /**
     * Returns the result list of given typed query as a map mapped by the given key mapper.
     * @param  The generic map key type.
     * @param  The generic result type, also map value type.
     * @param typedQuery The involved typed query.
     * @param keyMapper The key mapper.
     * @return The result list of given typed query as a map mapped by the given key mapper.
     */
    public static  Map getResultMap(TypedQuery typedQuery, Function keyMapper) {
        return typedQuery.getResultList().stream().collect(toMap(keyMapper));
    }

    /**
     * Returns the result list of given typed query as a map mapped by the given key and value mappers.
     * @param  The generic map key type.
     * @param  The generic result type.
     * @param  The generic map value type.
     * @param typedQuery The involved typed query.
     * @param keyMapper The key mapper.
     * @param valueMapper The value mapper.
     * @return The result list of given typed query as a map mapped by the given key and value mappers.
     */
    public static  Map getResultMap(TypedQuery typedQuery, Function keyMapper, Function valueMapper) {
        return typedQuery.getResultList().stream().collect(Collectors.toMap(keyMapper, valueMapper));
    }


    // Entity utils -----------------------------------------------------------------------------------------------------------------------

    /**
     * Returns the currently active {@link BaseEntityService} from the {@link SessionContext}.
     * @return The currently active {@link BaseEntityService} from the {@link SessionContext}.
     * @throws IllegalStateException if there is none, which can happen if this method is called outside EJB context,
     * or when currently invoked EJB service is not an instance of {@link BaseEntityService}.
     */
    @SuppressWarnings("unchecked")
    public static BaseEntityService getCurrentBaseEntityService() {
        try {
            var ejbContext = (SessionContext) new InitialContext().lookup("java:comp/EJBContext");
            return (BaseEntityService) ejbContext.getBusinessObject(ejbContext.getInvokedBusinessInterface());
        }
        catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Returns count of all foreign key references to entity of given entity type with given ID of given identifier type.
     * This is particularly useful in case you intend to check if the given entity is still referenced elsewhere in database.
     * @param  The generic result type.
     * @param  The generic identifier type.
     * @param entityManager The involved entity manager.
     * @param entityType Entity type.
     * @param identifierType Identifier type.
     * @param id Entity ID.
     * @return Count of all foreign key references to entity of given entity type with given ID of given identifier type.
     */
    public static  long countForeignKeyReferences(EntityManager entityManager, Class entityType, Class identifierType, I id) {
        var metamodel = entityManager.getMetamodel();
        SingularAttribute idAttribute = metamodel.entity(entityType).getId(identifierType);
        return metamodel.getEntities().stream()
            .flatMap(entity -> getAttributesOfType(entity, entityType))
            .distinct()
            .mapToLong(attribute -> countReferencesTo(entityManager, attribute, idAttribute, id))
            .sum();
    }

    private static  Stream> getAttributesOfType(EntityType entity, Class entityType) {
        return entity.getAttributes().stream()
            .filter(attribute -> entityType.equals(getJavaType(attribute)))
            .map(attribute -> attribute);
    }

    private static  Class getJavaType(Attribute attribute) {
        return attribute instanceof PluralAttribute
            ? ((PluralAttribute) attribute).getElementType().getJavaType()
            : attribute.getJavaType();
    }

    @SuppressWarnings("unchecked")
    private static  Long countReferencesTo(EntityManager entityManager, Attribute attribute, SingularAttribute idAttribute, I id) {
        var criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery query = criteriaBuilder.createQuery(Long.class);
        Root root = query.from(attribute.getDeclaringType().getJavaType());
        Join join;

        if (attribute instanceof SingularAttribute) {
            join = root.join((SingularAttribute) attribute);
        }
        else if (attribute instanceof ListAttribute) {
            join = root.join((ListAttribute) attribute);
        }
        else if (attribute instanceof SetAttribute) {
            join = root.join((SetAttribute) attribute);
        }
        else if (attribute instanceof MapAttribute) {
            join = root.join((MapAttribute) attribute);
        }
        else if (attribute instanceof CollectionAttribute) {
            join = root.join((CollectionAttribute) attribute);
        }
        else {
            return 0L; // Unknown attribute type, just return 0.
        }

        query.select(criteriaBuilder.count(root)).where(criteriaBuilder.equal(join.get(idAttribute), id));
        return entityManager.createQuery(query).getSingleResult();
    }


    // Criteria utils ---------------------------------------------------------------------------------------------------------------------

    /**
     * Returns a SQL CONCAT(...) of given expressions or strings.
     * @param builder The involved criteria builder.
     * @param expressionsOrStrings Expressions or Strings.
     * @return A SQL CONCAT(...) of given expressions or strings.
     * @throws IllegalArgumentException When there are less than 2 expressions or strings. There's no point of concat then.
     */
    public static Expression concat(CriteriaBuilder builder, Object... expressionsOrStrings) {
        if (expressionsOrStrings.length < 2) {
            throw new IllegalArgumentException("There must be at least 2 expressions or strings");
        }

        List> expressions = stream(expressionsOrStrings).map(expressionOrString -> {
            if (expressionOrString instanceof Expression) {
                return castAsString(builder, (Expression) expressionOrString);
            }
            else {
                return builder.literal(expressionOrString);
            }
        }).collect(toList());

        return builder.function("CONCAT", String.class, expressions.toArray(new Expression[expressions.size()]));
    }

    /**
     * Returns a new expression wherein given expression is cast as String.
     * This covers known problems with certain providers and/or databases.
     * @param builder The involved criteria builder.
     * @param expression Expression to be cast as String.
     * @return A new expression wherein given expression is cast as String.
     */
    @SuppressWarnings("unchecked")
    public static Expression castAsString(CriteriaBuilder builder, Expression expression) {
        if (Provider.is(HIBERNATE)) {
            return expression.as(String.class);
        }

        // EclipseLink and OpenJPA have a broken Expression#as() implementation, need to delegate to DB specific function.

        // PostgreSQL is quite strict in string casting, it has to be performed explicitly.
        if (Database.is(POSTGRESQL)) {
            String pattern = null;

            if (Numeric.is(expression.getJavaType())) {
                pattern = "FM999999999999999999"; // NOTE: Amount of digits matches amount of Long.MAX_VALUE digits minus one.
            }
            else if (LocalDate.class.isAssignableFrom(expression.getJavaType())) {
                pattern = "YYYY-MM-DD";
            }
            else if (LocalTime.class.isAssignableFrom(expression.getJavaType())) {
                pattern = "HH24:MI:SS";
            }
            else if (LocalDateTime.class.isAssignableFrom(expression.getJavaType())) {
                pattern = "YYYY-MM-DD'T'HH24:MI:SS'Z'"; // NOTE: PostgreSQL uses ISO_INSTANT instead of ISO_LOCAL_DATE_TIME.
            }
            else if (OffsetTime.class.isAssignableFrom(expression.getJavaType())) {
                pattern = "HH24:MI:SS-OF"; // TODO: PostgreSQL doc says it's not supported?
            }
            else if (OffsetDateTime.class.isAssignableFrom(expression.getJavaType())) {
                pattern = "YYYY-MM-DD'T'HH24:MI:SS-OF";
            }

            if (pattern != null) {
                return builder.function("TO_CHAR", String.class, expression, builder.literal(pattern));
            }
        }

        // H2 and MySQL are more lenient in this, they can do implicit casting with sane defaults, so no custom function call necessary.
        return (Expression) expression;
    }

    /**
     * Returns whether given path is {@link Enumerated} by {@link EnumType#ORDINAL}.
     * @param path Path of interest.
     * @return Whether given path is {@link Enumerated} by {@link EnumType#ORDINAL}.
     */
    public static boolean isEnumeratedByOrdinal(Path path) {
        Bindable model = path.getModel();

        if (model instanceof Attribute) {
            var member = ((Attribute) model).getJavaMember();

            if (member instanceof AnnotatedElement) {
                var enumerated = ((AnnotatedElement) member).getAnnotation(Enumerated.class);

                if (enumerated != null) {
                    return enumerated.value() == EnumType.ORDINAL;
                }
            }
        }

        return false;
    }

}