Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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.service;
import static jakarta.persistence.CacheRetrieveMode.BYPASS;
import static jakarta.persistence.metamodel.PluralAttribute.CollectionType.MAP;
import static java.lang.Integer.MAX_VALUE;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableSet;
import static java.util.Optional.ofNullable;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.FINER;
import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.IntStream.range;
import static org.omnifaces.persistence.Database.POSTGRESQL;
import static org.omnifaces.persistence.JPA.QUERY_HINT_CACHE_RETRIEVE_MODE;
import static org.omnifaces.persistence.JPA.QUERY_HINT_CACHE_STORE_MODE;
import static org.omnifaces.persistence.JPA.QUERY_HINT_LOAD_GRAPH;
import static org.omnifaces.persistence.JPA.countForeignKeyReferences;
import static org.omnifaces.persistence.JPA.getOptionalFirstResult;
import static org.omnifaces.persistence.JPA.getValidationMode;
import static org.omnifaces.persistence.Provider.ECLIPSELINK;
import static org.omnifaces.persistence.Provider.HIBERNATE;
import static org.omnifaces.persistence.Provider.OPENJPA;
import static org.omnifaces.persistence.Provider.QUERY_HINT_ECLIPSELINK_MAINTAIN_CACHE;
import static org.omnifaces.persistence.Provider.QUERY_HINT_ECLIPSELINK_REFRESH;
import static org.omnifaces.persistence.Provider.QUERY_HINT_HIBERNATE_CACHEABLE;
import static org.omnifaces.persistence.model.Identifiable.ID;
import static org.omnifaces.utils.Lang.capitalize;
import static org.omnifaces.utils.Lang.coalesce;
import static org.omnifaces.utils.Lang.isEmpty;
import static org.omnifaces.utils.Lang.startsWithOneOf;
import static org.omnifaces.utils.reflect.Reflections.getActualTypeArguments;
import static org.omnifaces.utils.reflect.Reflections.invokeGetter;
import static org.omnifaces.utils.reflect.Reflections.invokeMethod;
import static org.omnifaces.utils.reflect.Reflections.invokeSetter;
import static org.omnifaces.utils.reflect.Reflections.listAnnotatedFields;
import static org.omnifaces.utils.reflect.Reflections.map;
import static org.omnifaces.utils.stream.Streams.stream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.annotation.PostConstruct;
import jakarta.ejb.Stateless;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.NonUniqueResultException;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.ValidationMode;
import jakarta.persistence.criteria.AbstractQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.FetchParent;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Order;
import jakarta.persistence.criteria.ParameterExpression;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Subquery;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.PluralAttribute.CollectionType;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
import org.omnifaces.persistence.Database;
import org.omnifaces.persistence.Provider;
import org.omnifaces.persistence.criteria.Bool;
import org.omnifaces.persistence.criteria.Criteria;
import org.omnifaces.persistence.criteria.Criteria.ParameterBuilder;
import org.omnifaces.persistence.criteria.Enumerated;
import org.omnifaces.persistence.criteria.IgnoreCase;
import org.omnifaces.persistence.criteria.Not;
import org.omnifaces.persistence.criteria.Numeric;
import org.omnifaces.persistence.exception.IllegalEntityStateException;
import org.omnifaces.persistence.exception.NonDeletableEntityException;
import org.omnifaces.persistence.exception.NonSoftDeletableEntityException;
import org.omnifaces.persistence.model.BaseEntity;
import org.omnifaces.persistence.model.GeneratedIdEntity;
import org.omnifaces.persistence.model.NonDeletable;
import org.omnifaces.persistence.model.SoftDeletable;
import org.omnifaces.persistence.model.TimestampedBaseEntity;
import org.omnifaces.persistence.model.TimestampedEntity;
import org.omnifaces.persistence.model.VersionedBaseEntity;
import org.omnifaces.persistence.model.VersionedEntity;
import org.omnifaces.persistence.model.dto.Page;
import org.omnifaces.utils.collection.PartialResultList;
import org.omnifaces.utils.reflect.Getter;
/**
*
* Base entity service. Let your {@link Stateless} service classes extend from this. Ideally, you would not anymore have
* the need to inject the {@link EntityManager} in your service class and it would suffice to just delegate all
* persistence actions to methods of this abstract class.
*
* You only need to let your entities extend from one of the following mapped super classes:
*
*
{@link BaseEntity}
*
{@link TimestampedBaseEntity}
*
{@link VersionedBaseEntity}
*
{@link GeneratedIdEntity}
*
{@link TimestampedEntity}
*
{@link VersionedEntity}
*
*
*
Logging
*
* {@link BaseEntityService} uses JULI {@link Logger} for logging.
*
*
{@link Level#FINER} will log the {@link #getPage(Page, boolean)} arguments, the set parameter values and the full query result.
*
{@link Level#FINE} will log computed type mapping (the actual values of I and E type paramters), and
* whether the ID is generated, and whether the entity is {@link SoftDeletable}, and
* any discovered {@link ElementCollection}, {@link ManyToOne}, {@link OneToOne} and {@link OneToMany} mappings of the entity. This is
* internally used in order to be able to build proper queries to perform a search inside those fields.
*
{@link Level#WARNING} will log unparseable or illegal criteria values. The {@link BaseEntityService} will skip them and continue.
*
{@link Level#SEVERE} will log constraint violations wrapped in any {@link ConstraintViolationException} during
* {@link #persist(BaseEntity)} and {@link #update(BaseEntity)}. Due to technical limitations, it will during update() only
* happen when jakarta.persistence.validation.mode property in persistence.xml is explicitly set to
* CALLBACK (and thus not to its default of AUTO).
*
*
* @param The generic ID type.
* @param The generic base entity type.
* @see BaseEntity
* @see Page
* @see Criteria
*/
public abstract class BaseEntityService & Serializable, E extends BaseEntity> {
private static final Logger logger = Logger.getLogger(BaseEntityService.class.getName());
private static final String LOG_FINER_GET_PAGE = "Get page: %s, count=%s, cacheable=%s, resultType=%s";
private static final String LOG_FINER_SET_PARAMETER_VALUES = "Set parameter values: %s";
private static final String LOG_FINER_QUERY_RESULT = "Query result: %s, estimatedTotalNumberOfResults=%s";
private static final String LOG_FINE_COMPUTED_TYPE_MAPPING = "Computed type mapping for %s: <%s, %s>";
private static final String LOG_FINE_COMPUTED_GENERATED_ID_MAPPING = "Computed generated ID mapping for %s: %s";
private static final String LOG_FINE_COMPUTED_SOFT_DELETE_MAPPING = "Computed soft delete mapping for %s: %s";
private static final String LOG_FINE_COMPUTED_ELEMENTCOLLECTION_MAPPING = "Computed @ElementCollection mapping for %s: %s";
private static final String LOG_FINE_COMPUTED_MANY_OR_ONE_TO_ONE_MAPPING = "Computed @ManyToOne/@OneToOne mapping for %s: %s";
private static final String LOG_FINE_COMPUTED_ONE_TO_MANY_MAPPING = "Computed @OneToMany mapping for %s: %s";
private static final String LOG_WARNING_ILLEGAL_CRITERIA_VALUE = "Cannot parse predicate for %s(%s) = %s(%s), skipping!";
private static final String LOG_SEVERE_CONSTRAINT_VIOLATION = "jakarta.validation.ConstraintViolation: @%s %s#%s %s on %s";
private static final String ERROR_ILLEGAL_MAPPING =
"You must return a getter-path mapping from MappedQueryBuilder";
private static final String ERROR_UNSUPPORTED_CRITERIA =
"Predicate for %s(%s) = %s(%s) is not supported. Consider wrapping in a Criteria instance or creating a custom one if you want to deal with it.";
private static final String ERROR_UNSUPPORTED_ONETOMANY_ORDERBY_ECLIPSELINK =
"Sorry, EclipseLink does not support sorting a @OneToMany or @ElementCollection relationship. Consider using a DTO or a DB view instead.";
private static final String ERROR_UNSUPPORTED_ONETOMANY_ORDERBY_OPENJPA =
"Sorry, OpenJPA does not support sorting a @OneToMany or @ElementCollection relationship. Consider using a DTO or a DB view instead.";
private static final String ERROR_UNSUPPORTED_ONETOMANY_CRITERIA_ECLIPSELINK =
"Sorry, EclipseLink does not support searching in a @OneToMany relationship. Consider using a DTO or a DB view instead.";
@SuppressWarnings("rawtypes")
private static final Map, Entry, Class>>> TYPE_MAPPINGS = new ConcurrentHashMap<>();
private static final Map>, Boolean> GENERATED_ID_MAPPINGS = new ConcurrentHashMap<>();
private static final Map>, SoftDeleteData> SOFT_DELETE_MAPPINGS = new ConcurrentHashMap<>();
private static final Map>, Set> ELEMENT_COLLECTION_MAPPINGS = new ConcurrentHashMap<>();
private static final Map>, Set> MANY_OR_ONE_TO_ONE_MAPPINGS = new ConcurrentHashMap<>();
private static final Map>, Set> ONE_TO_MANY_MAPPINGS = new ConcurrentHashMap<>();
private final Class identifierType;
private final Class entityType;
private final boolean generatedId;
private final SoftDeleteData softDeleteData;
private Provider provider = Provider.UNKNOWN;
private Database database = Database.UNKNOWN;
private Supplier> elementCollections = Collections::emptySet;
private Supplier> manyOrOneToOnes = Collections::emptySet;
private java.util.function.Predicate oneToManys = field -> false;
private Validator validator;
@PersistenceContext
private EntityManager entityManager;
// Init -----------------------------------------------------------------------------------------------------------
/**
* The constructor initializes the type mapping.
* The I and E will be resolved to a concrete Class<?>.
*/
@SuppressWarnings("unchecked")
protected BaseEntityService() {
var typeMapping = TYPE_MAPPINGS.computeIfAbsent(getClass(), BaseEntityService::computeTypeMapping);
identifierType = (Class) typeMapping.getKey();
entityType = (Class) typeMapping.getValue();
generatedId = GENERATED_ID_MAPPINGS.computeIfAbsent(entityType, BaseEntityService::computeGeneratedIdMapping);
softDeleteData = SOFT_DELETE_MAPPINGS.computeIfAbsent(entityType, BaseEntityService::computeSoftDeleteMapping);
}
/**
* The postconstruct initializes the properties dependent on entity manager.
*/
@PostConstruct
protected void initWithEntityManager() {
provider = Provider.of(getEntityManager());
database = Database.of(getEntityManager());
elementCollections = () -> ELEMENT_COLLECTION_MAPPINGS.computeIfAbsent(entityType, this::computeElementCollectionMapping);
manyOrOneToOnes = () -> MANY_OR_ONE_TO_ONE_MAPPINGS.computeIfAbsent(entityType, this::computeManyOrOneToOneMapping);
oneToManys = field -> ONE_TO_MANY_MAPPINGS.computeIfAbsent(entityType, this::computeOneToManyMapping).stream().anyMatch(oneToMany -> field.startsWith(oneToMany + '.'));
if (getValidationMode(getEntityManager()) == ValidationMode.CALLBACK) {
validator = CDI.current().select(Validator.class).get();
}
}
@SuppressWarnings("rawtypes")
private static Entry, Class>> computeTypeMapping(Class extends BaseEntityService> subclass) {
var actualTypeArguments = getActualTypeArguments(subclass, BaseEntityService.class);
var identifierType = actualTypeArguments.get(0);
var entityType = actualTypeArguments.get(1);
logger.log(FINE, () -> format(LOG_FINE_COMPUTED_TYPE_MAPPING, subclass, identifierType, entityType));
return new SimpleEntry<>(identifierType, entityType);
}
private static boolean computeGeneratedIdMapping(Class> entityType) {
var generatedId = GeneratedIdEntity.class.isAssignableFrom(entityType) || !listAnnotatedFields(entityType, Id.class, GeneratedValue.class).isEmpty();
logger.log(FINE, () -> format(LOG_FINE_COMPUTED_GENERATED_ID_MAPPING, entityType, generatedId));
return generatedId;
}
private static SoftDeleteData computeSoftDeleteMapping(Class> entityType) {
var softDeleteData = new SoftDeleteData(entityType);
logger.log(FINE, () -> format(LOG_FINE_COMPUTED_SOFT_DELETE_MAPPING, entityType, softDeleteData));
return softDeleteData;
}
private Set computeElementCollectionMapping(Class extends BaseEntity>> entityType) {
var elementCollectionMapping = computeEntityMapping(entityType, "", new HashSet<>(), getProvider()::isElementCollection);
logger.log(FINE, () -> format(LOG_FINE_COMPUTED_ELEMENTCOLLECTION_MAPPING, entityType, elementCollectionMapping));
return elementCollectionMapping;
}
private Set computeManyOrOneToOneMapping(Class extends BaseEntity>> entityType) {
var manyOrOneToOneMapping = computeEntityMapping(entityType, "", new HashSet<>(), getProvider()::isManyOrOneToOne);
logger.log(FINE, () -> format(LOG_FINE_COMPUTED_MANY_OR_ONE_TO_ONE_MAPPING, entityType, manyOrOneToOneMapping));
return manyOrOneToOneMapping;
}
private Set computeOneToManyMapping(Class extends BaseEntity>> entityType) {
var oneToManyMapping = computeEntityMapping(entityType, "", new HashSet<>(), getProvider()::isOneToMany);
logger.log(FINE, () -> format(LOG_FINE_COMPUTED_ONE_TO_MANY_MAPPING, entityType, oneToManyMapping));
return oneToManyMapping;
}
private Set computeEntityMapping(Class> type, String basePath, Set> nestedTypes, java.util.function.Predicate> attributePredicate) {
var entityMapping = new HashSet(2);
var entity = getEntityManager().getMetamodel().entity(type);
for (var attribute : entity.getAttributes()) {
if (attributePredicate.test(attribute)) {
entityMapping.add(basePath + attribute.getName());
}
if (attribute instanceof Bindable> bindable) {
Class> nestedType = bindable.getBindableJavaType();
if (BaseEntity.class.isAssignableFrom(nestedType) && nestedType != entityType && nestedTypes.add(nestedType)) {
entityMapping.addAll(computeEntityMapping(nestedType, basePath + attribute.getName() + '.', nestedTypes, attributePredicate));
}
}
}
return unmodifiableSet(entityMapping);
}
// Getters --------------------------------------------------------------------------------------------------------
/**
* Returns the JPA provider being used. This is immutable (you can't override the method to change the internally used value).
* @return The JPA provider being used.
*/
public Provider getProvider() {
return provider;
}
/**
* Returns the SQL database being used. This is immutable (you can't override the method to change the internally used value).
* @return The SQL database being used.
*/
public Database getDatabase() {
return database;
}
/**
* Returns the actual type of the generic ID type I. This is immutable (you can't override the method to change the internally used value).
* @return The actual type of the generic ID type I.
*/
protected Class getIdentifierType() {
return identifierType;
}
/**
* Returns the actual type of the generic base entity type E. This is immutable (you can't override the method to change the internally used value).
* @return The actual type of the generic base entity type E.
*/
protected Class getEntityType() {
return entityType;
}
/**
* Returns whether the ID is generated. This is immutable (you can't override the method to change the internally used value).
* @return Whether the ID is generated.
*/
protected boolean isGeneratedId() {
return generatedId;
}
/**
* Returns the entity manager being used. When you have only one persistence unit, then you don't need to override
* this. When you have multiple persistence units, then you need to extend the {@link BaseEntityService} like below
* wherein you supply the persistence unit specific entity manager and then let all your service classes extend
* from it instead.
*
*
* @return The entity manager being used.
*/
protected EntityManager getEntityManager() {
return entityManager;
}
/**
* Returns the metamodel of current base entity.
* @return The metamodel of current base entity.
*/
protected EntityType getMetamodel() {
return getEntityManager().getMetamodel().entity(entityType);
}
/**
* Returns the metamodel of given base entity.
* @param The generic ID type of the given base entity.
* @param The generic base entity type of the given base entity.
* @param entity Base entity to obtain metamodel for.
* @return The metamodel of given base entity.
*/
@SuppressWarnings({ "unchecked", "hiding" })
public & Serializable, E extends BaseEntity> EntityType getMetamodel(E entity) {
return getEntityManager().getMetamodel().entity((Class) entity.getClass());
}
// Preparing actions ----------------------------------------------------------------------------------------------
/**
* Create an instance of {@link TypedQuery} for executing a Java Persistence Query Language statement identified
* by the given name, usually to perform a SELECT e.
* @param name The name of the Java Persistence Query Language statement defined in metadata, which can be either
* a {@link NamedQuery} or a <persistence-unit><mapping-file>.
* @return An instance of {@link TypedQuery} for executing a Java Persistence Query Language statement identified
* by the given name, usually to perform a SELECT e.
*/
protected TypedQuery createNamedTypedQuery(String name) {
return getEntityManager().createNamedQuery(name, entityType);
}
/**
* Create an instance of {@link Query} for executing a Java Persistence Query Language statement identified
* by the given name, usually to perform an INSERT, UPDATE or DELETE.
* @param name The name of the Java Persistence Query Language statement defined in metadata, which can be either
* a {@link NamedQuery} or a <persistence-unit><mapping-file>.
* @return An instance of {@link Query} for executing a Java Persistence Query Language statement identified
* by the given name, usually to perform an INSERT, UPDATE or DELETE.
*/
protected Query createNamedQuery(String name) {
return getEntityManager().createNamedQuery(name);
}
/**
* Create an instance of {@link TypedQuery} for executing the given Java Persistence Query Language statement which
* returns the specified T, usually to perform a SELECT t.
* @param The generic result type.
* @param jpql The Java Persistence Query Language statement.
* @param resultType The result type.
* @return An instance of {@link TypedQuery} for executing the given Java Persistence Query Language statement which
* returns the specified T, usually to perform a SELECT t.
*/
protected TypedQuery createTypedQuery(String jpql, Class resultType) {
return getEntityManager().createQuery(jpql, resultType);
}
/**
* Create an instance of {@link TypedQuery} for executing the given Java Persistence Query Language statement which
* returns a E, usually to perform a SELECT e.
* @param jpql The Java Persistence Query Language statement.
* @return An instance of {@link TypedQuery} for executing the given Java Persistence Query Language statement which
* returns a E, usually to perform a SELECT e.
*/
protected TypedQuery createTypedQuery(String jpql) {
return createTypedQuery(jpql, entityType);
}
/**
* Create an instance of {@link TypedQuery} for executing the given Java Persistence Query Language statement which
* returns a Long, usually a SELECT e.id or SELECT COUNT(e).
* @param jpql The Java Persistence Query Language statement.
* @return An instance of {@link TypedQuery} for executing the given Java Persistence Query Language statement which
* returns a Long, usually a SELECT e.id or SELECT COUNT(e).
*/
protected TypedQuery createLongQuery(String jpql) {
return createTypedQuery(jpql, Long.class);
}
/**
* Create an instance of {@link Query} for executing the given Java Persistence Query Language statement,
* usually to perform an INSERT, UPDATE or DELETE.
* @param jpql The Java Persistence Query Language statement.
* @return An instance of {@link Query} for executing the given Java Persistence Query Language statement,
* usually to perform an INSERT, UPDATE or DELETE.
*/
protected Query createQuery(String jpql) {
return getEntityManager().createQuery(jpql);
}
// Select actions -------------------------------------------------------------------------------------------------
/**
* Functional interface to fine-grain a JPA criteria query for any of
* {@link #list(CriteriaQueryBuilder, Consumer)} or {@link #find(CriteriaQueryBuilder, Consumer)} methods.
*
* You do not need this interface directly. Just supply a lambda. Below is an usage example:
*
* @param The generic base entity type.
*/
@FunctionalInterface
protected interface CriteriaQueryBuilder {
void build(CriteriaBuilder criteriaBuilder, CriteriaQuery query, Root root);
}
/**
* Find entity by the given query and positional parameters, if any.
*
* Usage example:
*
* Optional<Foo> foo = find("SELECT f FROM Foo f WHERE f.bar = ?1 AND f.baz = ?2", bar, baz);
*
*
* Short jpql is also supported:
*
* Optional<Foo> foo = find("WHERE bar = ?1 AND baz = ?2", bar, baz);
*
* @param jpql The Java Persistence Query Language statement.
* @param parameters The positional query parameters, if any.
* @return Found entity matching the given query and positional parameters, if any.
* @throws NonUniqueResultException When more than one entity is found matching the given query and positional parameters.
*/
protected Optional find(String jpql, Object... parameters) {
return getOptionalSingleResult(list(jpql, parameters));
}
/**
* Find entity by the given query and mapped parameters, if any.
*
* Usage example:
*
* Optional<Foo> foo = find("SELECT f FROM Foo f WHERE f.bar = :bar AND f.baz = :baz", params -> {
* params.put("bar", bar);
* params.put("baz", baz);
* });
*
* @param jpql The Java Persistence Query Language statement.
* @param parameters To put the mapped query parameters in.
* @return Found entity matching the given query and mapped parameters, if any.
* @throws NonUniqueResultException When more than one entity is found matching the given query and mapped parameters.
*/
protected Optional find(String jpql, Consumer