From d7070e6211fac5d0222e77b79e61c72b61ce7f14 Mon Sep 17 00:00:00 2001 From: Bauke Scholtz Date: Sat, 14 Sep 2024 09:41:08 -0400 Subject: [PATCH] replace tabs by spaces, project wide --- pom.xml | 762 ++-- .../org/omnifaces/persistence/Database.java | 56 +- .../java/org/omnifaces/persistence/JPA.java | 654 +-- .../org/omnifaces/persistence/Provider.java | 420 +- .../omnifaces/persistence/audit/Audit.java | 2 +- .../persistence/audit/AuditListener.java | 142 +- .../persistence/criteria/Between.java | 58 +- .../omnifaces/persistence/criteria/Bool.java | 84 +- .../persistence/criteria/Criteria.java | 184 +- .../persistence/criteria/Enumerated.java | 62 +- .../persistence/criteria/IgnoreCase.java | 34 +- .../omnifaces/persistence/criteria/Like.java | 184 +- .../omnifaces/persistence/criteria/Not.java | 44 +- .../persistence/criteria/Numeric.java | 84 +- .../omnifaces/persistence/criteria/Order.java | 206 +- .../datasource/CommonDataSourceWrapper.java | 460 +- .../datasource/PropertiesFileLoader.java | 4 +- .../SwitchableCommonDataSource.java | 162 +- .../datasource/SwitchableXADataSource.java | 32 +- .../omnifaces/persistence/event/Created.java | 2 +- .../omnifaces/persistence/event/Deleted.java | 2 +- .../omnifaces/persistence/event/Updated.java | 2 +- .../exception/BaseEntityException.java | 20 +- .../IllegalEntityStateException.java | 8 +- .../NonDeletableEntityException.java | 8 +- .../NonSoftDeletableEntityException.java | 8 +- .../listener/BaseEntityListener.java | 94 +- .../persistence/model/BaseEntity.java | 58 +- .../persistence/model/GeneratedIdEntity.java | 22 +- .../persistence/model/Identifiable.java | 28 +- .../persistence/model/NonDeletable.java | 2 +- .../persistence/model/SoftDeletable.java | 48 +- .../persistence/model/Timestamped.java | 8 +- .../model/TimestampedBaseEntity.java | 106 +- .../persistence/model/TimestampedEntity.java | 106 +- .../persistence/model/Versioned.java | 2 +- .../model/VersionedBaseEntity.java | 18 +- .../persistence/model/VersionedEntity.java | 18 +- .../omnifaces/persistence/model/dto/Page.java | 552 +-- .../omnifaces/persistence/service/Alias.java | 102 +- .../service/BaseEntityService.java | 4028 ++++++++--------- .../persistence/service/EclipseLinkRoot.java | 50 +- .../persistence/service/FetchWrapper.java | 122 +- .../persistence/service/JoinFetchAdapter.java | 114 +- .../service/MappedPathResolver.java | 30 +- .../persistence/service/PageBuilder.java | 74 +- .../persistence/service/PathResolver.java | 8 +- .../persistence/service/PostponedFetch.java | 26 +- .../persistence/service/RootPathResolver.java | 216 +- .../persistence/service/RootWrapper.java | 534 +-- .../persistence/service/SoftDeleteData.java | 128 +- .../persistence/service/SubqueryRoot.java | 126 +- .../service/UncheckedParameterBuilder.java | 36 +- src/main/resources/META-INF/beans.xml | 8 +- .../persistence/test/OmniPersistenceTest.java | 488 +- .../persistence/test/model/Address.java | 72 +- .../persistence/test/model/Comment.java | 18 +- .../persistence/test/model/Gender.java | 8 +- .../persistence/test/model/Group.java | 8 +- .../persistence/test/model/Lookup.java | 52 +- .../persistence/test/model/Person.java | 94 +- .../persistence/test/model/Phone.java | 64 +- .../persistence/test/model/Text.java | 18 +- .../test/model/dto/PersonCard.java | 30 +- .../test/service/PersonService.java | 86 +- .../test/service/PhoneService.java | 12 +- .../test/service/StartupService.java | 126 +- src/test/resources/META-INF/persistence.xml | 64 +- src/test/resources/web.xml | 24 +- 69 files changed, 5756 insertions(+), 5756 deletions(-) diff --git a/pom.xml b/pom.xml index 9f7d8f8..0a36a4a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,114 +1,114 @@ - 4.0.0 + 4.0.0 - org.omnifaces - omnipersistence - 0.21.J1 - jar + org.omnifaces + omnipersistence + 0.21.J1 + jar - OmniPersistence - Utilities for JPA, JDBC and DataSources - https://github.com/omnifaces/omnipersistence - - OmniFaces - https://omnifaces.org - - 2015 + OmniPersistence + Utilities for JPA, JDBC and DataSources + https://github.com/omnifaces/omnipersistence + + OmniFaces + https://omnifaces.org + + 2015 - - - balusc - Bauke Scholtz - balusc@gmail.com - - - arjan.tijms - Arjan Tijms - arjan.tijms@gmail.com - - - jan.beernink - Jan Beernink - jan.beernink@gmail.com - - + + + balusc + Bauke Scholtz + balusc@gmail.com + + + arjan.tijms + Arjan Tijms + arjan.tijms@gmail.com + + + jan.beernink + Jan Beernink + jan.beernink@gmail.com + + - - - The Apache Software License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - repo - - + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + - - https://github.com/omnifaces/omnipersistence - scm:git:git://github.com/omnifaces/omnipersistence.git - scm:git:git@github.com:omnifaces/omnipersistence.git - + + https://github.com/omnifaces/omnipersistence + scm:git:git://github.com/omnifaces/omnipersistence.git + scm:git:git@github.com:omnifaces/omnipersistence.git + - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + - + - - 17 - 10 + + 17 + 10 - - UTF-8 - UTF-8 - ${javase.version} - true + + UTF-8 + UTF-8 + ${javase.version} + true - - 33.0.1.Final - 7.0.17 - 1.4.200 - + + 33.0.1.Final + 7.0.17 + 1.4.200 + - + - - - jakarta.platform - jakarta.jakartaee-web-api - ${jakartaee.version}.0.0 - provided - + + + jakarta.platform + jakarta.jakartaee-web-api + ${jakartaee.version}.0.0 + provided + - - - org.omnifaces - omniutils - 0.12 - - - - - org.hibernate.orm - hibernate-jpamodelgen - 6.6.0.Final - provided - + + + org.omnifaces + omniutils + 0.12 + + + + + org.hibernate.orm + hibernate-jpamodelgen + 6.6.0.Final + provided + - - - org.junit.jupiter - junit-jupiter - 5.11.0 - test - + + + org.junit.jupiter + junit-jupiter + 5.11.0 + test + org.jboss.arquillian.junit5 arquillian-junit5-container @@ -121,10 +121,10 @@ 3.3.1 test - + - - + + @@ -148,29 +148,29 @@ - - - com.mycila - license-maven-plugin - 4.5 - -
license.txt
- - *.* - - - SLASHSTAR_STYLE - -
- - - process-sources - - format - - - -
+ + + com.mycila + license-maven-plugin + 4.5 + +
license.txt
+ + *.* + + + SLASHSTAR_STYLE + +
+ + + process-sources + + format + + + +
@@ -179,74 +179,74 @@ 3.13.0 - - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - - test-jar - - - - - - - true - true - - - ${project.url} - ${project.artifactId} - - - - + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + test-jar + + + + + + + true + true + + + ${project.url} + ${project.artifactId} + + + + - - - org.apache.maven.plugins - maven-source-plugin - 3.3.1 - - - attach-sources - - jar-no-fork - - - - + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.10.0 - - ${java.home}/bin/javadoc - ${javase.version} - true - true + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.10.0 + + ${java.home}/bin/javadoc + ${javase.version} + true + true all,-missing - OmniPersistence API documentation - - https://jakarta.ee/specifications/platform/${jakartaee.version}/apidocs/ - - - - - attach-javadocs - - jar - - - - + OmniPersistence API documentation + + https://jakarta.ee/specifications/platform/${jakartaee.version}/apidocs/ + + + + + attach-javadocs + + jar + + + + - + org.sonatype.plugins nexus-staging-maven-plugin @@ -258,210 +258,210 @@ true -
+
- - + + - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - com.mycila - license-maven-plugin - [4.4,) - - format - - - - - true - - - - - - - + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + com.mycila + license-maven-plugin + [4.4,) + + format + + + + + true + + + + + + + - - - org.apache.maven.plugins - maven-dependency-plugin - 3.8.0 - - - process-test-classes - - unpack - - - - - ${project.build.directory} - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.0 - - false - - ${project.activeProfiles[0].id} - ${test.h2.version} - - - - - -
+ + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.0 + + + process-test-classes + + unpack + + + + + ${project.build.directory} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.0 + + false + + ${project.activeProfiles[0].id} + ${test.h2.version} + + + + + + - - - - + + + + - - - wildfly-hibernate - - true - - - - org.wildfly.arquillian - wildfly-arquillian-container-managed - 5.0.1.Final - test - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - - - org.wildfly - wildfly-preview-dist - ${test.wildfly.version} - zip - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - ${project.build.directory}/wildfly-preview-${test.wildfly.version} - - - - - - + + + wildfly-hibernate + + true + + + + org.wildfly.arquillian + wildfly-arquillian-container-managed + 5.0.1.Final + test + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + + + org.wildfly + wildfly-preview-dist + ${test.wildfly.version} + zip + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.build.directory}/wildfly-preview-${test.wildfly.version} + + + + + + - - - glassfish-eclipselink - - - org.omnifaces.arquillian - arquillian-glassfish-server-managed - 1.6 - test - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - - - org.glassfish.main.distributions - glassfish - ${test.glassfish.version} - zip - - - - - - install-h2-in-glassfish - process-test-classes - - copy - - - - - com.h2database - h2 - ${test.h2.version} - jar - ${project.build.directory}/glassfish7/glassfish/modules - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - ${project.build.directory}/glassfish7 - - - - - - + + + glassfish-eclipselink + + + org.omnifaces.arquillian + arquillian-glassfish-server-managed + 1.6 + test + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + + + org.glassfish.main.distributions + glassfish + ${test.glassfish.version} + zip + + + + + + install-h2-in-glassfish + process-test-classes + + copy + + + + + com.h2database + h2 + ${test.h2.version} + jar + ${project.build.directory}/glassfish7/glassfish/modules + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.build.directory}/glassfish7 + + + + + + - - - release - - + + + release + + - - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - - - - - + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + + + + +
\ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/Database.java b/src/main/java/org/omnifaces/persistence/Database.java index 86e8f20..e8ac88f 100644 --- a/src/main/java/org/omnifaces/persistence/Database.java +++ b/src/main/java/org/omnifaces/persistence/Database.java @@ -31,44 +31,44 @@ */ public enum Database { - H2, + H2, - MYSQL("MARIA"), + MYSQL("MARIA"), - POSTGRESQL("POSTGRES"), + POSTGRESQL("POSTGRES"), - UNKNOWN; + UNKNOWN; - private static final Logger logger = Logger.getLogger(Database.class.getName()); + private static final Logger logger = Logger.getLogger(Database.class.getName()); - private String[] names; + private String[] names; - private Database(String... aliases) { - this.names = concat(Stream.of(name()), stream(aliases)).collect(toList()).toArray(new String[0]); - } + private Database(String... aliases) { + this.names = concat(Stream.of(name()), stream(aliases)).collect(toList()).toArray(new String[0]); + } - public static Database of(EntityManager entityManager) { - Provider provider = Provider.of(entityManager); - EntityManagerFactory entityManagerFactory = entityManager.getEntityManagerFactory(); + public static Database of(EntityManager entityManager) { + Provider provider = Provider.of(entityManager); + EntityManagerFactory entityManagerFactory = entityManager.getEntityManagerFactory(); - try { - String uppercasedDialectName = provider.getDialectName(entityManagerFactory).toUpperCase(); + try { + String uppercasedDialectName = provider.getDialectName(entityManagerFactory).toUpperCase(); - for (Database database : values()) { - if (startsWithOneOf(uppercasedDialectName, database.names)) { - return database; - } - } - } - catch (Exception e) { - logger.log(WARNING, "Cannot to determine configured Database for " + provider + " by " + entityManagerFactory, e); - } + for (Database database : values()) { + if (startsWithOneOf(uppercasedDialectName, database.names)) { + return database; + } + } + } + catch (Exception e) { + logger.log(WARNING, "Cannot to determine configured Database for " + provider + " by " + entityManagerFactory, e); + } - return UNKNOWN; - } + return UNKNOWN; + } - public static boolean is(Database database) { - return BaseEntityService.getCurrentInstance().getDatabase() == database; - } + public static boolean is(Database database) { + return BaseEntityService.getCurrentInstance().getDatabase() == database; + } } diff --git a/src/main/java/org/omnifaces/persistence/JPA.java b/src/main/java/org/omnifaces/persistence/JPA.java index 274fa41..91d32a6 100644 --- a/src/main/java/org/omnifaces/persistence/JPA.java +++ b/src/main/java/org/omnifaces/persistence/JPA.java @@ -67,332 +67,332 @@ @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) { - Object 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 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) { - Metamodel 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) { - CriteriaBuilder 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) { - Member member = ((Attribute) model).getJavaMember(); - - if (member instanceof AnnotatedElement) { - Enumerated enumerated = ((AnnotatedElement) member).getAnnotation(Enumerated.class); - - if (enumerated != null) { - return enumerated.value() == EnumType.ORDINAL; - } - } - } - - return false; - } + // 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) { + Object 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 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) { + Metamodel 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) { + CriteriaBuilder 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) { + Member member = ((Attribute) model).getJavaMember(); + + if (member instanceof AnnotatedElement) { + Enumerated enumerated = ((AnnotatedElement) member).getAnnotation(Enumerated.class); + + if (enumerated != null) { + return enumerated.value() == EnumType.ORDINAL; + } + } + } + + return false; + } } diff --git a/src/main/java/org/omnifaces/persistence/Provider.java b/src/main/java/org/omnifaces/persistence/Provider.java index 444cae9..a5155fb 100644 --- a/src/main/java/org/omnifaces/persistence/Provider.java +++ b/src/main/java/org/omnifaces/persistence/Provider.java @@ -45,217 +45,217 @@ */ public enum Provider { - HIBERNATE { - - @Override - public String getDialectName(EntityManagerFactory entityManagerFactory) { - var unwrappedEntityManagerFactory = unwrapEntityManagerFactoryIfNecessary(entityManagerFactory); - - if (HIBERNATE_SESSION_FACTORY.get().isInstance(unwrappedEntityManagerFactory)) { - // 5.2+ has merged hibernate-entitymanager into hibernate-core, and made EntityManagerFactory impl an instance of SessionFactory, and removed getDialect() shortcut method. - return invokeMethod(invokeMethod(invokeMethod(unwrappedEntityManagerFactory, "getJdbcServices"), "getJdbcEnvironment"), "getDialect").getClass().getSimpleName(); - } - else { - return invokeMethod(invokeMethod(unwrappedEntityManagerFactory, "getSessionFactory"), "getDialect").getClass().getSimpleName(); - } - } - - @Override - public boolean isAggregation(Expression expression) { - if (HIBERNATE_6_0_0_AGGREGATE_FUNCTION.isPresent()) { + HIBERNATE { + + @Override + public String getDialectName(EntityManagerFactory entityManagerFactory) { + var unwrappedEntityManagerFactory = unwrapEntityManagerFactoryIfNecessary(entityManagerFactory); + + if (HIBERNATE_SESSION_FACTORY.get().isInstance(unwrappedEntityManagerFactory)) { + // 5.2+ has merged hibernate-entitymanager into hibernate-core, and made EntityManagerFactory impl an instance of SessionFactory, and removed getDialect() shortcut method. + return invokeMethod(invokeMethod(invokeMethod(unwrappedEntityManagerFactory, "getJdbcServices"), "getJdbcEnvironment"), "getDialect").getClass().getSimpleName(); + } + else { + return invokeMethod(invokeMethod(unwrappedEntityManagerFactory, "getSessionFactory"), "getDialect").getClass().getSimpleName(); + } + } + + @Override + public boolean isAggregation(Expression expression) { + if (HIBERNATE_6_0_0_AGGREGATE_FUNCTION.isPresent()) { return HIBERNATE_6_0_0_AGGREGATE_FUNCTION.get().isInstance(expression); - } - else { - return HIBERNATE_BASIC_FUNCTION_EXPRESSION.get().isInstance(expression) && (boolean) invokeMethod(expression, "isAggregation") - || HIBERNATE_COMPARISON_PREDICATE.get().isInstance(expression) && (isAggregation(invokeMethod(expression, "getLeftHandOperand")) || isAggregation(invokeMethod(expression, "getRightHandOperand"))); - } - } - - @Override - public & Serializable, E extends BaseEntity> boolean isProxy(E entity) { - return HIBERNATE_PROXY.get().isInstance(entity); - } - - @Override - public & Serializable, E extends BaseEntity> boolean isProxyUninitialized(E entity) { - return invokeOnProxy(entity, "isUninitialized", super::isProxyUninitialized); - } - - @Override - public & Serializable, E extends BaseEntity> E dereferenceProxy(E entity) { - return invokeOnProxy(entity, "getImplementation", super::dereferenceProxy); - } - - @Override - public & Serializable, E extends BaseEntity> Class getEntityType(E entity) { - return invokeOnProxy(entity, "getPersistentClass", super::getEntityType); - } - - @Override - public & Serializable, E extends BaseEntity> I getIdentifier(E entity) { - return invokeOnProxy(entity, "getIdentifier", super::getIdentifier); - } - - @SuppressWarnings("unchecked") - private & Serializable, E extends BaseEntity> T invokeOnProxy(E entity, String methodName, Function fallback) { - return isProxy(entity) ? (T) invokeMethod(invokeMethod(entity, "getHibernateLazyInitializer"), methodName) : fallback.apply(entity); - } - }, - - ECLIPSELINK { - - @Override - public String getDialectName(EntityManagerFactory entityManagerFactory) { - var unwrappedEntityManagerFactory = unwrapEntityManagerFactoryIfNecessary(entityManagerFactory); - return invokeMethod(invokeMethod(unwrappedEntityManagerFactory, "getDatabaseSession"), "getDatasourcePlatform").getClass().getSimpleName(); - } - - @Override - public boolean isAggregation(Expression expression) { - return ECLIPSELINK_FUNCTION_EXPRESSION_IMPL.get().isInstance(expression) && AGGREGATE_FUNCTIONS.contains(invokeMethod(expression, "getOperation")); - } - }, - - OPENJPA { - - @Override - public String getDialectName(EntityManagerFactory entityManagerFactory) { - var unwrappedEntityManagerFactory = unwrapEntityManagerFactoryIfNecessary(entityManagerFactory); - return invokeMethod(invokeMethod(unwrappedEntityManagerFactory, "getConfiguration"), "getDBDictionaryInstance").getClass().getSimpleName(); - } - - @Override - public boolean isAggregation(Expression expression) { - // We could also invoke toValue() on it and then isAggregate(), but that requires ExpressionFactory and CriteriaQueryImpl arguments which are not trivial to get here. - return AGGREGATE_FUNCTIONS.contains(expression.getClass().getSimpleName().toUpperCase()); - } - - @Override - public boolean isElementCollection(Attribute attribute) { - // For some reason OpenJPA returns PersistentAttributeType.ONE_TO_MANY on an @ElementCollection. - return ((Field) attribute.getJavaMember()).getAnnotation(ElementCollection.class) != null; - } - - @Override - public boolean isOneToMany(Attribute attribute) { - // For some reason OpenJPA returns PersistentAttributeType.ONE_TO_MANY on an @ElementCollection. - return !isElementCollection(attribute) && super.isOneToMany(attribute); - } - }, - - UNKNOWN; - - public static final String QUERY_HINT_HIBERNATE_CACHEABLE = "org.hibernate.cacheable"; // true | false - public static final String QUERY_HINT_HIBERNATE_CACHE_REGION = "org.hibernate.cacheRegion"; // 2nd level cache region ID - public static final String QUERY_HINT_ECLIPSELINK_MAINTAIN_CACHE = "eclipselink.maintain-cache"; // true | false - public static final String QUERY_HINT_ECLIPSELINK_REFRESH = "eclipselink.refresh"; // true | false - - private static final Optional> HIBERNATE_PROXY = findClass("org.hibernate.proxy.HibernateProxy"); - private static final Optional> HIBERNATE_SESSION_FACTORY = findClass("org.hibernate.SessionFactory"); - private static final Optional> HIBERNATE_3_5_0_BASIC_FUNCTION_EXPRESSION = findClass("org.hibernate.ejb.criteria.expression.function.BasicFunctionExpression"); - private static final Optional> HIBERNATE_4_3_0_BASIC_FUNCTION_EXPRESSION = findClass("org.hibernate.jpa.criteria.expression.function.BasicFunctionExpression"); - private static final Optional> HIBERNATE_5_2_0_BASIC_FUNCTION_EXPRESSION = findClass("org.hibernate.query.criteria.internal.expression.function.BasicFunctionExpression"); - private static final Optional> HIBERNATE_BASIC_FUNCTION_EXPRESSION = Stream.of(HIBERNATE_5_2_0_BASIC_FUNCTION_EXPRESSION, HIBERNATE_4_3_0_BASIC_FUNCTION_EXPRESSION, HIBERNATE_3_5_0_BASIC_FUNCTION_EXPRESSION).filter(Optional::isPresent).findFirst().orElse(Optional.empty()); + } + else { + return HIBERNATE_BASIC_FUNCTION_EXPRESSION.get().isInstance(expression) && (boolean) invokeMethod(expression, "isAggregation") + || HIBERNATE_COMPARISON_PREDICATE.get().isInstance(expression) && (isAggregation(invokeMethod(expression, "getLeftHandOperand")) || isAggregation(invokeMethod(expression, "getRightHandOperand"))); + } + } + + @Override + public & Serializable, E extends BaseEntity> boolean isProxy(E entity) { + return HIBERNATE_PROXY.get().isInstance(entity); + } + + @Override + public & Serializable, E extends BaseEntity> boolean isProxyUninitialized(E entity) { + return invokeOnProxy(entity, "isUninitialized", super::isProxyUninitialized); + } + + @Override + public & Serializable, E extends BaseEntity> E dereferenceProxy(E entity) { + return invokeOnProxy(entity, "getImplementation", super::dereferenceProxy); + } + + @Override + public & Serializable, E extends BaseEntity> Class getEntityType(E entity) { + return invokeOnProxy(entity, "getPersistentClass", super::getEntityType); + } + + @Override + public & Serializable, E extends BaseEntity> I getIdentifier(E entity) { + return invokeOnProxy(entity, "getIdentifier", super::getIdentifier); + } + + @SuppressWarnings("unchecked") + private & Serializable, E extends BaseEntity> T invokeOnProxy(E entity, String methodName, Function fallback) { + return isProxy(entity) ? (T) invokeMethod(invokeMethod(entity, "getHibernateLazyInitializer"), methodName) : fallback.apply(entity); + } + }, + + ECLIPSELINK { + + @Override + public String getDialectName(EntityManagerFactory entityManagerFactory) { + var unwrappedEntityManagerFactory = unwrapEntityManagerFactoryIfNecessary(entityManagerFactory); + return invokeMethod(invokeMethod(unwrappedEntityManagerFactory, "getDatabaseSession"), "getDatasourcePlatform").getClass().getSimpleName(); + } + + @Override + public boolean isAggregation(Expression expression) { + return ECLIPSELINK_FUNCTION_EXPRESSION_IMPL.get().isInstance(expression) && AGGREGATE_FUNCTIONS.contains(invokeMethod(expression, "getOperation")); + } + }, + + OPENJPA { + + @Override + public String getDialectName(EntityManagerFactory entityManagerFactory) { + var unwrappedEntityManagerFactory = unwrapEntityManagerFactoryIfNecessary(entityManagerFactory); + return invokeMethod(invokeMethod(unwrappedEntityManagerFactory, "getConfiguration"), "getDBDictionaryInstance").getClass().getSimpleName(); + } + + @Override + public boolean isAggregation(Expression expression) { + // We could also invoke toValue() on it and then isAggregate(), but that requires ExpressionFactory and CriteriaQueryImpl arguments which are not trivial to get here. + return AGGREGATE_FUNCTIONS.contains(expression.getClass().getSimpleName().toUpperCase()); + } + + @Override + public boolean isElementCollection(Attribute attribute) { + // For some reason OpenJPA returns PersistentAttributeType.ONE_TO_MANY on an @ElementCollection. + return ((Field) attribute.getJavaMember()).getAnnotation(ElementCollection.class) != null; + } + + @Override + public boolean isOneToMany(Attribute attribute) { + // For some reason OpenJPA returns PersistentAttributeType.ONE_TO_MANY on an @ElementCollection. + return !isElementCollection(attribute) && super.isOneToMany(attribute); + } + }, + + UNKNOWN; + + public static final String QUERY_HINT_HIBERNATE_CACHEABLE = "org.hibernate.cacheable"; // true | false + public static final String QUERY_HINT_HIBERNATE_CACHE_REGION = "org.hibernate.cacheRegion"; // 2nd level cache region ID + public static final String QUERY_HINT_ECLIPSELINK_MAINTAIN_CACHE = "eclipselink.maintain-cache"; // true | false + public static final String QUERY_HINT_ECLIPSELINK_REFRESH = "eclipselink.refresh"; // true | false + + private static final Optional> HIBERNATE_PROXY = findClass("org.hibernate.proxy.HibernateProxy"); + private static final Optional> HIBERNATE_SESSION_FACTORY = findClass("org.hibernate.SessionFactory"); + private static final Optional> HIBERNATE_3_5_0_BASIC_FUNCTION_EXPRESSION = findClass("org.hibernate.ejb.criteria.expression.function.BasicFunctionExpression"); + private static final Optional> HIBERNATE_4_3_0_BASIC_FUNCTION_EXPRESSION = findClass("org.hibernate.jpa.criteria.expression.function.BasicFunctionExpression"); + private static final Optional> HIBERNATE_5_2_0_BASIC_FUNCTION_EXPRESSION = findClass("org.hibernate.query.criteria.internal.expression.function.BasicFunctionExpression"); + private static final Optional> HIBERNATE_BASIC_FUNCTION_EXPRESSION = Stream.of(HIBERNATE_5_2_0_BASIC_FUNCTION_EXPRESSION, HIBERNATE_4_3_0_BASIC_FUNCTION_EXPRESSION, HIBERNATE_3_5_0_BASIC_FUNCTION_EXPRESSION).filter(Optional::isPresent).findFirst().orElse(Optional.empty()); private static final Optional> HIBERNATE_6_0_0_AGGREGATE_FUNCTION = findClass("org.hibernate.query.sqm.function.SelfRenderingSqmAggregateFunction"); - private static final Optional> HIBERNATE_3_5_0_COMPARISON_PREDICATE = findClass("org.hibernate.ejb.criteria.predicate.ComparisonPredicate"); - private static final Optional> HIBERNATE_4_3_0_COMPARISON_PREDICATE = findClass("org.hibernate.jpa.criteria.predicate.ComparisonPredicate"); - private static final Optional> HIBERNATE_5_2_0_COMPARISON_PREDICATE = findClass("org.hibernate.query.criteria.internal.predicate.ComparisonPredicate"); - private static final Optional> HIBERNATE_COMPARISON_PREDICATE = Stream.of(HIBERNATE_5_2_0_COMPARISON_PREDICATE, HIBERNATE_4_3_0_COMPARISON_PREDICATE, HIBERNATE_3_5_0_COMPARISON_PREDICATE).filter(Optional::isPresent).findFirst().orElse(Optional.empty()); - private static final Optional> ECLIPSELINK_FUNCTION_EXPRESSION_IMPL = findClass("org.eclipse.persistence.internal.jpa.querydef.FunctionExpressionImpl"); - private static final Set AGGREGATE_FUNCTIONS = unmodifiableSet("MIN", "MAX", "SUM", "AVG", "COUNT"); - - private static Object unwrapEntityManagerFactoryIfNecessary(EntityManagerFactory entityManagerFactory) { - var packageName = entityManagerFactory.getClass().getPackage().getName(); - - if (packageName.startsWith("org.apache.openejb.")) { - var getDelegate = findMethod(entityManagerFactory, "getDelegate"); - return getDelegate.isPresent() ? invokeMethod(entityManagerFactory, getDelegate.get()) : entityManagerFactory; - } - - return entityManagerFactory; - } - - public static Provider of(EntityManager entityManager) { - var packageName = entityManager.getDelegate().getClass().getPackage().getName(); - - if (packageName.startsWith("org.hibernate.")) { - return HIBERNATE; - } - else if (packageName.startsWith("org.eclipse.persistence.")) { - return ECLIPSELINK; - } - else if (packageName.startsWith("org.apache.openjpa.")) { - return OPENJPA; - } - else { - return UNKNOWN; - } - } - - public static boolean is(Provider provider) { - return BaseEntityService.getCurrentInstance().getProvider() == provider; - } - - public String getDialectName(EntityManagerFactory entityManagerFactory) { - throw new UnsupportedOperationException(String.valueOf(entityManagerFactory)); - } - - public boolean isAggregation(Expression expression) { - throw new UnsupportedOperationException(String.valueOf(expression)); - } - - public boolean isElementCollection(Attribute attribute) { - return attribute.getPersistentAttributeType() == ELEMENT_COLLECTION; - } - - public boolean isOneToMany(Attribute attribute) { - return attribute.getPersistentAttributeType() == ONE_TO_MANY; - } - - public boolean isManyOrOneToOne(Attribute attribute) { - return isOneOf(attribute.getPersistentAttributeType(), MANY_TO_ONE, ONE_TO_ONE); - } - - public & Serializable, E extends BaseEntity> boolean isProxy(E entity) { - return false; - } - - public & Serializable, E extends BaseEntity> boolean isProxyUninitialized(E entity) { - return false; - } - - public & Serializable, E extends BaseEntity> E dereferenceProxy(E entity) { - return entity; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public & Serializable, E extends BaseEntity> Class getEntityType(E entity) { - if (entity == null) { - return null; - } - - Class entityType = entity.getClass(); - - while (BaseEntity.class.isAssignableFrom(entityType) && entityType.getAnnotation(Entity.class) == null) - { - entityType = (Class) entityType.getSuperclass(); - } - - return (Class) entityType; - } - - public & Serializable, E extends BaseEntity> I getIdentifier(E entity) { - return entity == null ? null : entity.getId(); - } - - public & Serializable, E extends BaseEntity> String getTableName(E entity) { - if (entity == null) { - return null; - } - - Class entityType = getEntityType(entity); - var table = entityType.getAnnotation(Table.class); - return table != null ? table.name() : entityType.getSimpleName().toUpperCase(); - } + private static final Optional> HIBERNATE_3_5_0_COMPARISON_PREDICATE = findClass("org.hibernate.ejb.criteria.predicate.ComparisonPredicate"); + private static final Optional> HIBERNATE_4_3_0_COMPARISON_PREDICATE = findClass("org.hibernate.jpa.criteria.predicate.ComparisonPredicate"); + private static final Optional> HIBERNATE_5_2_0_COMPARISON_PREDICATE = findClass("org.hibernate.query.criteria.internal.predicate.ComparisonPredicate"); + private static final Optional> HIBERNATE_COMPARISON_PREDICATE = Stream.of(HIBERNATE_5_2_0_COMPARISON_PREDICATE, HIBERNATE_4_3_0_COMPARISON_PREDICATE, HIBERNATE_3_5_0_COMPARISON_PREDICATE).filter(Optional::isPresent).findFirst().orElse(Optional.empty()); + private static final Optional> ECLIPSELINK_FUNCTION_EXPRESSION_IMPL = findClass("org.eclipse.persistence.internal.jpa.querydef.FunctionExpressionImpl"); + private static final Set AGGREGATE_FUNCTIONS = unmodifiableSet("MIN", "MAX", "SUM", "AVG", "COUNT"); + + private static Object unwrapEntityManagerFactoryIfNecessary(EntityManagerFactory entityManagerFactory) { + var packageName = entityManagerFactory.getClass().getPackage().getName(); + + if (packageName.startsWith("org.apache.openejb.")) { + var getDelegate = findMethod(entityManagerFactory, "getDelegate"); + return getDelegate.isPresent() ? invokeMethod(entityManagerFactory, getDelegate.get()) : entityManagerFactory; + } + + return entityManagerFactory; + } + + public static Provider of(EntityManager entityManager) { + var packageName = entityManager.getDelegate().getClass().getPackage().getName(); + + if (packageName.startsWith("org.hibernate.")) { + return HIBERNATE; + } + else if (packageName.startsWith("org.eclipse.persistence.")) { + return ECLIPSELINK; + } + else if (packageName.startsWith("org.apache.openjpa.")) { + return OPENJPA; + } + else { + return UNKNOWN; + } + } + + public static boolean is(Provider provider) { + return BaseEntityService.getCurrentInstance().getProvider() == provider; + } + + public String getDialectName(EntityManagerFactory entityManagerFactory) { + throw new UnsupportedOperationException(String.valueOf(entityManagerFactory)); + } + + public boolean isAggregation(Expression expression) { + throw new UnsupportedOperationException(String.valueOf(expression)); + } + + public boolean isElementCollection(Attribute attribute) { + return attribute.getPersistentAttributeType() == ELEMENT_COLLECTION; + } + + public boolean isOneToMany(Attribute attribute) { + return attribute.getPersistentAttributeType() == ONE_TO_MANY; + } + + public boolean isManyOrOneToOne(Attribute attribute) { + return isOneOf(attribute.getPersistentAttributeType(), MANY_TO_ONE, ONE_TO_ONE); + } + + public & Serializable, E extends BaseEntity> boolean isProxy(E entity) { + return false; + } + + public & Serializable, E extends BaseEntity> boolean isProxyUninitialized(E entity) { + return false; + } + + public & Serializable, E extends BaseEntity> E dereferenceProxy(E entity) { + return entity; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public & Serializable, E extends BaseEntity> Class getEntityType(E entity) { + if (entity == null) { + return null; + } + + Class entityType = entity.getClass(); + + while (BaseEntity.class.isAssignableFrom(entityType) && entityType.getAnnotation(Entity.class) == null) + { + entityType = (Class) entityType.getSuperclass(); + } + + return (Class) entityType; + } + + public & Serializable, E extends BaseEntity> I getIdentifier(E entity) { + return entity == null ? null : entity.getId(); + } + + public & Serializable, E extends BaseEntity> String getTableName(E entity) { + if (entity == null) { + return null; + } + + Class entityType = getEntityType(entity); + var table = entityType.getAnnotation(Table.class); + return table != null ? table.name() : entityType.getSimpleName().toUpperCase(); + } } diff --git a/src/main/java/org/omnifaces/persistence/audit/Audit.java b/src/main/java/org/omnifaces/persistence/audit/Audit.java index 6bde74b..37f0623 100644 --- a/src/main/java/org/omnifaces/persistence/audit/Audit.java +++ b/src/main/java/org/omnifaces/persistence/audit/Audit.java @@ -48,5 +48,5 @@ @Retention(RUNTIME) @Target(FIELD) public @interface Audit { - // + // } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/audit/AuditListener.java b/src/main/java/org/omnifaces/persistence/audit/AuditListener.java index 673cf6e..a0ccfb4 100644 --- a/src/main/java/org/omnifaces/persistence/audit/AuditListener.java +++ b/src/main/java/org/omnifaces/persistence/audit/AuditListener.java @@ -46,84 +46,84 @@ */ public abstract class AuditListener & Serializable> { - private static final Map, Map>> AUDITABLE_PROPERTIES = new ConcurrentHashMap<>(); + private static final Map, Map>> AUDITABLE_PROPERTIES = new ConcurrentHashMap<>(); - @PostLoad - public void beforeUpdate(BaseEntity entity) { - getAuditableProperties(entity).forEach((property, values) -> values.put(entity.getId(), invokeMethod(entity, property.getReadMethod()))); - } + @PostLoad + public void beforeUpdate(BaseEntity entity) { + getAuditableProperties(entity).forEach((property, values) -> values.put(entity.getId(), invokeMethod(entity, property.getReadMethod()))); + } - @PreUpdate - public void afterUpdate(BaseEntity entity) { - getAuditableProperties(entity).forEach((property, values) -> { - if (values.containsKey(entity.getId())) { - Object newValue = invokeMethod(entity, property.getReadMethod()); - Object oldValue = values.remove(entity.getId()); + @PreUpdate + public void afterUpdate(BaseEntity entity) { + getAuditableProperties(entity).forEach((property, values) -> { + if (values.containsKey(entity.getId())) { + Object newValue = invokeMethod(entity, property.getReadMethod()); + Object oldValue = values.remove(entity.getId()); - if (!Objects.equals(oldValue, newValue)) { - saveAuditedChange(entity, property, oldValue, newValue); - } - } - }); - } + if (!Objects.equals(oldValue, newValue)) { + saveAuditedChange(entity, property, oldValue, newValue); + } + } + }); + } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Map> getAuditableProperties(BaseEntity entity) { - Map auditableProperties = AUDITABLE_PROPERTIES.computeIfAbsent(entity.getClass(), k -> { - BaseEntityService baseEntityService = BaseEntityService.getCurrentInstance(); - Set auditablePropertyNames = baseEntityService.getMetamodel(entity).getDeclaredAttributes().stream() - .filter(a -> a.getJavaMember() instanceof Field && ((Field) a.getJavaMember()).isAnnotationPresent(Audit.class)) - .map(Attribute::getName) - .collect(toSet()); + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Map> getAuditableProperties(BaseEntity entity) { + Map auditableProperties = AUDITABLE_PROPERTIES.computeIfAbsent(entity.getClass(), k -> { + BaseEntityService baseEntityService = BaseEntityService.getCurrentInstance(); + Set auditablePropertyNames = baseEntityService.getMetamodel(entity).getDeclaredAttributes().stream() + .filter(a -> a.getJavaMember() instanceof Field && ((Field) a.getJavaMember()).isAnnotationPresent(Audit.class)) + .map(Attribute::getName) + .collect(toSet()); - try { - return stream(getBeanInfo(baseEntityService.getProvider().getEntityType(entity)).getPropertyDescriptors()) - .filter(p -> auditablePropertyNames.contains(p.getName())) - .collect(toConcurrentMap(identity(), v -> new ConcurrentHashMap<>())); - } - catch (IntrospectionException e) { - throw new UnsupportedOperationException(e); - } - }); + try { + return stream(getBeanInfo(baseEntityService.getProvider().getEntityType(entity)).getPropertyDescriptors()) + .filter(p -> auditablePropertyNames.contains(p.getName())) + .collect(toConcurrentMap(identity(), v -> new ConcurrentHashMap<>())); + } + catch (IntrospectionException e) { + throw new UnsupportedOperationException(e); + } + }); - return auditableProperties; - } + return auditableProperties; + } - /** - *

- * Example implementation: - *

-	 * YourAuditedChange yourAuditedChange = new YourAuditedChange();
-	 * yourAuditedChange.setTimestamp(Instant.now());
-	 * yourAuditedChange.setUser(activeUser);
-	 * yourAuditedChange.setEntityName(entityManager.getMetamodel().entity(entity.getClass()).getName());
-	 * yourAuditedChange.setEntityId(entity.getId());
-	 * yourAuditedChange.setPropertyName(property.getName());
-	 * yourAuditedChange.setOldValue(oldValue != null ? oldValue.toString() : null);
-	 * yourAuditedChange.setNewValue(newValue != null ? newValue.toString() : null);
-	 * inject(YourAuditedChangeService.class).persist(yourAuditedChange);
-	 * 
- * - * @param entity The parent entity. - * @param property The audited property. - * @param oldValue The old value. - * @param newValue The new value. - */ - protected abstract void saveAuditedChange(BaseEntity entity, PropertyDescriptor property, Object oldValue, Object newValue); + /** + *

+ * Example implementation: + *

+     * YourAuditedChange yourAuditedChange = new YourAuditedChange();
+     * yourAuditedChange.setTimestamp(Instant.now());
+     * yourAuditedChange.setUser(activeUser);
+     * yourAuditedChange.setEntityName(entityManager.getMetamodel().entity(entity.getClass()).getName());
+     * yourAuditedChange.setEntityId(entity.getId());
+     * yourAuditedChange.setPropertyName(property.getName());
+     * yourAuditedChange.setOldValue(oldValue != null ? oldValue.toString() : null);
+     * yourAuditedChange.setNewValue(newValue != null ? newValue.toString() : null);
+     * inject(YourAuditedChangeService.class).persist(yourAuditedChange);
+     * 
+ * + * @param entity The parent entity. + * @param property The audited property. + * @param oldValue The old value. + * @param newValue The new value. + */ + protected abstract void saveAuditedChange(BaseEntity entity, PropertyDescriptor property, Object oldValue, Object newValue); - /** - *

- * Work around for CDI inject not working in JPA EntityListener. Usage: - *

-	 * YourAuditedChangeService service = inject(YourAuditedChangeService.class);
-	 * 
- * - * @param The generic CDI managed bean type. - * @param type The CDI managed bean type. - * @return The CDI managed instance. - */ - protected static T inject(Class type) { - return CDI.current().select(type).get(); - } + /** + *

+ * Work around for CDI inject not working in JPA EntityListener. Usage: + *

+     * YourAuditedChangeService service = inject(YourAuditedChangeService.class);
+     * 
+ * + * @param The generic CDI managed bean type. + * @param type The CDI managed bean type. + * @return The CDI managed instance. + */ + protected static T inject(Class type) { + return CDI.current().select(type).get(); + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/criteria/Between.java b/src/main/java/org/omnifaces/persistence/criteria/Between.java index 1c843e1..e177919 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/Between.java +++ b/src/main/java/org/omnifaces/persistence/criteria/Between.java @@ -25,34 +25,34 @@ */ public final class Between> extends Criteria> { - private Between(Range value) { - super(value); - } - - public static > Between value(Range value) { - return new Between<>(value); - } - - public static > Between range(T min, T max) { - return new Between<>(Range.ofClosed(min, max)); - } - - @Override - @SuppressWarnings("unchecked") - public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { - return criteriaBuilder.between((Expression) path, parameterBuilder.create(getValue().getMin()), parameterBuilder.create(getValue().getMax())); - } - - @Override - @SuppressWarnings("unchecked") - public boolean applies(Object modelValue) { - return modelValue instanceof Comparable && getValue().contains((T) modelValue); - } - - @Override - public String toString() { - Range> range = getValue(); - return "BETWEEN " + range.getMin() + " AND " + range.getMax(); - } + private Between(Range value) { + super(value); + } + + public static > Between value(Range value) { + return new Between<>(value); + } + + public static > Between range(T min, T max) { + return new Between<>(Range.ofClosed(min, max)); + } + + @Override + @SuppressWarnings("unchecked") + public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { + return criteriaBuilder.between((Expression) path, parameterBuilder.create(getValue().getMin()), parameterBuilder.create(getValue().getMax())); + } + + @Override + @SuppressWarnings("unchecked") + public boolean applies(Object modelValue) { + return modelValue instanceof Comparable && getValue().contains((T) modelValue); + } + + @Override + public String toString() { + Range> range = getValue(); + return "BETWEEN " + range.getMin() + " AND " + range.getMax(); + } } diff --git a/src/main/java/org/omnifaces/persistence/criteria/Bool.java b/src/main/java/org/omnifaces/persistence/criteria/Bool.java index dd21eea..d73f549 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/Bool.java +++ b/src/main/java/org/omnifaces/persistence/criteria/Bool.java @@ -26,54 +26,54 @@ */ public final class Bool extends Criteria { - private Bool(Boolean value) { - super(value); - } + private Bool(Boolean value) { + super(value); + } - public static Bool value(Boolean value) { - return new Bool(value); - } + public static Bool value(Boolean value) { + return new Bool(value); + } - public static Bool parse(Object searchValue) { - return new Bool(isTruthy(searchValue)); - } + public static Bool parse(Object searchValue) { + return new Bool(isTruthy(searchValue)); + } - public static boolean is(Class type) { - return type == boolean.class || Boolean.class.isAssignableFrom(type); - } + public static boolean is(Class type) { + return type == boolean.class || Boolean.class.isAssignableFrom(type); + } - @Override - @SuppressWarnings("unchecked") - public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { - Predicate predicate = criteriaBuilder.isTrue((Expression) path); - return getValue() ? predicate : criteriaBuilder.not(predicate); - } + @Override + @SuppressWarnings("unchecked") + public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { + Predicate predicate = criteriaBuilder.isTrue((Expression) path); + return getValue() ? predicate : criteriaBuilder.not(predicate); + } - @Override - public boolean applies(Object modelValue) { - return Objects.equals(isTruthy(modelValue), getValue()); - } + @Override + public boolean applies(Object modelValue) { + return Objects.equals(isTruthy(modelValue), getValue()); + } - public static Boolean isTruthy(Object value) { - if (value == null) { - return false; - } - if (value instanceof Boolean) { - return (Boolean) value; - } - else if (value instanceof Number) { - return ((Number) value).intValue() > 0; - } - else { - String valueAsString = value.toString(); + public static Boolean isTruthy(Object value) { + if (value == null) { + return false; + } + if (value instanceof Boolean) { + return (Boolean) value; + } + else if (value instanceof Number) { + return ((Number) value).intValue() > 0; + } + else { + String valueAsString = value.toString(); - try { - return new BigDecimal(valueAsString).intValue() > 0; - } - catch (NumberFormatException ignore) { - return Boolean.parseBoolean(valueAsString); - } - } - } + try { + return new BigDecimal(valueAsString).intValue() > 0; + } + catch (NumberFormatException ignore) { + return Boolean.parseBoolean(valueAsString); + } + } + } } diff --git a/src/main/java/org/omnifaces/persistence/criteria/Criteria.java b/src/main/java/org/omnifaces/persistence/criteria/Criteria.java index 05e5905..0f177c7 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/Criteria.java +++ b/src/main/java/org/omnifaces/persistence/criteria/Criteria.java @@ -50,97 +50,97 @@ */ public abstract class Criteria { - private T value; - - /** - * Create criteria based on given value. - * @param value The criteria value. - * @throws IllegalArgumentException When given criteria value cannot be reasonably parsed. - */ - protected Criteria(T value) { - this(value, false); - } - - Criteria(T value, boolean nestable) { - if (value instanceof Criteria && (!nestable || value.getClass() == getClass())) { - throw new IllegalArgumentException("You cannot nest " + value + " in " + this); - } - - if (!nestable && value == null) { - throw new NullPointerException("value"); - } - - this.value = value; - } - - /** - * Returns a predicate for the criteria value. Below is an example implementation: - *
-	 * return criteriaBuilder.equal(path, parameterBuilder.create(getValue()));
-	 * 
- * @param path Entity property path. You can use this to inspect the target entity property. - * @param criteriaBuilder So you can build a predicate with a {@link ParameterExpression}. - * @param parameterBuilder You must use this to create a {@link ParameterExpression} for the criteria value. - * @return A predicate for the criteria value. - */ - public abstract Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder); - - /** - * Returns whether this criteria value would apply to the given model value. This must basically represent the "plain Java" - * equivalent of the SQL behavior as achieved by {@link #build(Expression, CriteriaBuilder, ParameterBuilder)}. - * @param modelValue The model value to test this criteria on. - * @return Whether this criteria value would apply to the given model value. - * @throws IllegalArgumentException When given model value cannot be reasonably parsed. - * @throws UnsupportedOperationException When this method is not implemented yet. - */ - public boolean applies(Object modelValue) { - throw new UnsupportedOperationException("This method is not implemented yet."); - } - - /** - * Returns the criteria value. - * @return The criteria value. - */ - public T getValue() { - return value; - } - - /** - * Unwraps the criteria value from given object which could possibly represent a {@link Criteria}. - * @param possibleCriteria Any object which could possibly represent a {@link Criteria}. - * @return The unwrapped criteria value when given object actually represents a {@link Criteria}, else the original value unmodified. - */ - public static Object unwrap(Object possibleCriteria) { - Object value = possibleCriteria; - - while (value instanceof Criteria) { - value = ((Criteria) possibleCriteria).getValue(); - } - - return value; - } - - @Override - public int hashCode() { - return Objects.hash(getClass(), value); - } - - @Override - public boolean equals(Object object) { - return getClass().isInstance(object) && (object == this || (Objects.equals(value, ((Criteria) object).value))); - } - - @Override - public String toString() { - return getClass().getSimpleName().toUpperCase() + "(" + getValue() + ")"; - } - - /** - * This is used in {@link Criteria#build(Expression, CriteriaBuilder, ParameterBuilder)}. - */ - @FunctionalInterface - public interface ParameterBuilder { - ParameterExpression create(Object value); - } + private T value; + + /** + * Create criteria based on given value. + * @param value The criteria value. + * @throws IllegalArgumentException When given criteria value cannot be reasonably parsed. + */ + protected Criteria(T value) { + this(value, false); + } + + Criteria(T value, boolean nestable) { + if (value instanceof Criteria && (!nestable || value.getClass() == getClass())) { + throw new IllegalArgumentException("You cannot nest " + value + " in " + this); + } + + if (!nestable && value == null) { + throw new NullPointerException("value"); + } + + this.value = value; + } + + /** + * Returns a predicate for the criteria value. Below is an example implementation: + *
+     * return criteriaBuilder.equal(path, parameterBuilder.create(getValue()));
+     * 
+ * @param path Entity property path. You can use this to inspect the target entity property. + * @param criteriaBuilder So you can build a predicate with a {@link ParameterExpression}. + * @param parameterBuilder You must use this to create a {@link ParameterExpression} for the criteria value. + * @return A predicate for the criteria value. + */ + public abstract Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder); + + /** + * Returns whether this criteria value would apply to the given model value. This must basically represent the "plain Java" + * equivalent of the SQL behavior as achieved by {@link #build(Expression, CriteriaBuilder, ParameterBuilder)}. + * @param modelValue The model value to test this criteria on. + * @return Whether this criteria value would apply to the given model value. + * @throws IllegalArgumentException When given model value cannot be reasonably parsed. + * @throws UnsupportedOperationException When this method is not implemented yet. + */ + public boolean applies(Object modelValue) { + throw new UnsupportedOperationException("This method is not implemented yet."); + } + + /** + * Returns the criteria value. + * @return The criteria value. + */ + public T getValue() { + return value; + } + + /** + * Unwraps the criteria value from given object which could possibly represent a {@link Criteria}. + * @param possibleCriteria Any object which could possibly represent a {@link Criteria}. + * @return The unwrapped criteria value when given object actually represents a {@link Criteria}, else the original value unmodified. + */ + public static Object unwrap(Object possibleCriteria) { + Object value = possibleCriteria; + + while (value instanceof Criteria) { + value = ((Criteria) possibleCriteria).getValue(); + } + + return value; + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), value); + } + + @Override + public boolean equals(Object object) { + return getClass().isInstance(object) && (object == this || (Objects.equals(value, ((Criteria) object).value))); + } + + @Override + public String toString() { + return getClass().getSimpleName().toUpperCase() + "(" + getValue() + ")"; + } + + /** + * This is used in {@link Criteria#build(Expression, CriteriaBuilder, ParameterBuilder)}. + */ + @FunctionalInterface + public interface ParameterBuilder { + ParameterExpression create(Object value); + } } diff --git a/src/main/java/org/omnifaces/persistence/criteria/Enumerated.java b/src/main/java/org/omnifaces/persistence/criteria/Enumerated.java index ba5d45a..9c9f75c 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/Enumerated.java +++ b/src/main/java/org/omnifaces/persistence/criteria/Enumerated.java @@ -25,42 +25,42 @@ */ public final class Enumerated extends Criteria> { - private Enumerated(Enum value) { - super(value); - } + private Enumerated(Enum value) { + super(value); + } - public static Enumerated value(Enum value) { - return new Enumerated(value); - } + public static Enumerated value(Enum value) { + return new Enumerated(value); + } - public static Enumerated parse(Object searchValue, Class> targetType) { - return new Enumerated(parseEnum(searchValue, targetType)); - } + public static Enumerated parse(Object searchValue, Class> targetType) { + return new Enumerated(parseEnum(searchValue, targetType)); + } - @Override - public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { - return criteriaBuilder.equal(path, parameterBuilder.create(getValue())); - } + @Override + public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { + return criteriaBuilder.equal(path, parameterBuilder.create(getValue())); + } - @Override - public boolean applies(Object modelValue) { - return modelValue != null && Objects.equals(parseEnum(modelValue, getValue().getClass()), getValue()); - } + @Override + public boolean applies(Object modelValue) { + return modelValue != null && Objects.equals(parseEnum(modelValue, getValue().getClass()), getValue()); + } - @SuppressWarnings("unchecked") - private static Enum parseEnum(Object searchValue, Class targetType) throws IllegalArgumentException { - if (searchValue instanceof Enum) { - return (Enum) searchValue; - } - else if (targetType.isEnum()) { - for (Enum enumConstant : ((Class>) targetType).getEnumConstants()) { - if (enumConstant.name().equalsIgnoreCase(searchValue.toString())) { - return enumConstant; - } - } - } + @SuppressWarnings("unchecked") + private static Enum parseEnum(Object searchValue, Class targetType) throws IllegalArgumentException { + if (searchValue instanceof Enum) { + return (Enum) searchValue; + } + else if (targetType.isEnum()) { + for (Enum enumConstant : ((Class>) targetType).getEnumConstants()) { + if (enumConstant.name().equalsIgnoreCase(searchValue.toString())) { + return enumConstant; + } + } + } - throw new IllegalArgumentException(searchValue.toString()); - } + throw new IllegalArgumentException(searchValue.toString()); + } } diff --git a/src/main/java/org/omnifaces/persistence/criteria/IgnoreCase.java b/src/main/java/org/omnifaces/persistence/criteria/IgnoreCase.java index e665c28..0ec7314 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/IgnoreCase.java +++ b/src/main/java/org/omnifaces/persistence/criteria/IgnoreCase.java @@ -25,22 +25,22 @@ */ public final class IgnoreCase extends Criteria { - private IgnoreCase(String value) { - super(value); - } - - public static IgnoreCase value(String value) { - return new IgnoreCase(value); - } - - @Override - public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { - return criteriaBuilder.equal(criteriaBuilder.lower(castAsString(criteriaBuilder, path)), criteriaBuilder.lower(parameterBuilder.create(getValue()))); - } - - @Override - public boolean applies(Object modelValue) { - return modelValue != null && modelValue.toString().equalsIgnoreCase(getValue()); - } + private IgnoreCase(String value) { + super(value); + } + + public static IgnoreCase value(String value) { + return new IgnoreCase(value); + } + + @Override + public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { + return criteriaBuilder.equal(criteriaBuilder.lower(castAsString(criteriaBuilder, path)), criteriaBuilder.lower(parameterBuilder.create(getValue()))); + } + + @Override + public boolean applies(Object modelValue) { + return modelValue != null && modelValue.toString().equalsIgnoreCase(getValue()); + } } diff --git a/src/main/java/org/omnifaces/persistence/criteria/Like.java b/src/main/java/org/omnifaces/persistence/criteria/Like.java index fa59f68..865b237 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/Like.java +++ b/src/main/java/org/omnifaces/persistence/criteria/Like.java @@ -32,97 +32,97 @@ */ public final class Like extends Criteria { - private enum Type { - STARTS_WITH, - ENDS_WITH, - CONTAINS; - } - - private Type type; - - private Like(Type type, String value) { - super(value); - this.type = type; - } - - public static Like startsWith(String value) { - return new Like(Type.STARTS_WITH, value); - } - - public static Like endsWith(String value) { - return new Like(Type.ENDS_WITH, value); - } - - public static Like contains(String value) { - return new Like(Type.CONTAINS, value); - } - - public boolean startsWith() { - return type == Type.STARTS_WITH; - } - - public boolean endsWith() { - return type == Type.ENDS_WITH; - } - - public boolean contains() { - return type == Type.CONTAINS; - } - - @Override - @SuppressWarnings("unchecked") - public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { - Class type = path.getJavaType(); - - if (type.isEnum() && path instanceof Path && isEnumeratedByOrdinal((Path) path)) { - Set matches = stream(type.getEnumConstants()).filter(this::applies).collect(toSet()); - return matches.isEmpty() ? criteriaBuilder.notEqual(criteriaBuilder.literal(1), parameterBuilder.create(1)) : path.in(parameterBuilder.create(matches)); - } - else if (Bool.is(type)) { - Expression pathAsBoolean = (Expression) path; - return Bool.isTruthy(getValue()) ? criteriaBuilder.isTrue(pathAsBoolean) : criteriaBuilder.isFalse(pathAsBoolean); - } - else { - boolean lowercaseable = !Numeric.is(type); - String searchValue = (startsWith() ? "" : "%") + (lowercaseable ? getValue().toLowerCase() : getValue()) + (endsWith() ? "" : "%"); - Expression pathAsString = castAsString(criteriaBuilder, path); - return criteriaBuilder.like(lowercaseable ? criteriaBuilder.lower(pathAsString) : pathAsString, parameterBuilder.create(searchValue)); - } - } - - @Override - public boolean applies(Object modelValue) { - if (modelValue == null) { - return false; - } - - String lowerCasedValue = getValue().toLowerCase(); - String lowerCasedModelValue = modelValue.toString().toLowerCase(); - - if (startsWith()) { - return lowerCasedModelValue.startsWith(lowerCasedValue); - } - else if (endsWith()) { - return lowerCasedModelValue.endsWith(lowerCasedValue); - } - else { - return lowerCasedModelValue.contains(lowerCasedValue); - } - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), type); - } - - @Override - public boolean equals(Object object) { - return super.equals(object) && Objects.equals(type, ((Like) object).type); - } - - @Override - public String toString() { - return "LIKE " + (startsWith() ? "" : "%") + getValue() + (endsWith() ? "" : "%"); - } + private enum Type { + STARTS_WITH, + ENDS_WITH, + CONTAINS; + } + + private Type type; + + private Like(Type type, String value) { + super(value); + this.type = type; + } + + public static Like startsWith(String value) { + return new Like(Type.STARTS_WITH, value); + } + + public static Like endsWith(String value) { + return new Like(Type.ENDS_WITH, value); + } + + public static Like contains(String value) { + return new Like(Type.CONTAINS, value); + } + + public boolean startsWith() { + return type == Type.STARTS_WITH; + } + + public boolean endsWith() { + return type == Type.ENDS_WITH; + } + + public boolean contains() { + return type == Type.CONTAINS; + } + + @Override + @SuppressWarnings("unchecked") + public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { + Class type = path.getJavaType(); + + if (type.isEnum() && path instanceof Path && isEnumeratedByOrdinal((Path) path)) { + Set matches = stream(type.getEnumConstants()).filter(this::applies).collect(toSet()); + return matches.isEmpty() ? criteriaBuilder.notEqual(criteriaBuilder.literal(1), parameterBuilder.create(1)) : path.in(parameterBuilder.create(matches)); + } + else if (Bool.is(type)) { + Expression pathAsBoolean = (Expression) path; + return Bool.isTruthy(getValue()) ? criteriaBuilder.isTrue(pathAsBoolean) : criteriaBuilder.isFalse(pathAsBoolean); + } + else { + boolean lowercaseable = !Numeric.is(type); + String searchValue = (startsWith() ? "" : "%") + (lowercaseable ? getValue().toLowerCase() : getValue()) + (endsWith() ? "" : "%"); + Expression pathAsString = castAsString(criteriaBuilder, path); + return criteriaBuilder.like(lowercaseable ? criteriaBuilder.lower(pathAsString) : pathAsString, parameterBuilder.create(searchValue)); + } + } + + @Override + public boolean applies(Object modelValue) { + if (modelValue == null) { + return false; + } + + String lowerCasedValue = getValue().toLowerCase(); + String lowerCasedModelValue = modelValue.toString().toLowerCase(); + + if (startsWith()) { + return lowerCasedModelValue.startsWith(lowerCasedValue); + } + else if (endsWith()) { + return lowerCasedModelValue.endsWith(lowerCasedValue); + } + else { + return lowerCasedModelValue.contains(lowerCasedValue); + } + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), type); + } + + @Override + public boolean equals(Object object) { + return super.equals(object) && Objects.equals(type, ((Like) object).type); + } + + @Override + public String toString() { + return "LIKE " + (startsWith() ? "" : "%") + getValue() + (endsWith() ? "" : "%"); + } } diff --git a/src/main/java/org/omnifaces/persistence/criteria/Not.java b/src/main/java/org/omnifaces/persistence/criteria/Not.java index 4a89fa4..0505be2 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/Not.java +++ b/src/main/java/org/omnifaces/persistence/criteria/Not.java @@ -25,27 +25,27 @@ */ public final class Not extends Criteria { - private Not(Object value) { - super(value, true); - } - - public static Not value(Object value) { - return new Not(value); - } - - @Override - public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean applies(Object modelValue) { - if (modelValue instanceof Criteria) { - return !((Criteria) modelValue).applies(getValue()); - } - else { - return !Objects.equals(modelValue, getValue()); - } - } + private Not(Object value) { + super(value, true); + } + + public static Not value(Object value) { + return new Not(value); + } + + @Override + public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean applies(Object modelValue) { + if (modelValue instanceof Criteria) { + return !((Criteria) modelValue).applies(getValue()); + } + else { + return !Objects.equals(modelValue, getValue()); + } + } } diff --git a/src/main/java/org/omnifaces/persistence/criteria/Numeric.java b/src/main/java/org/omnifaces/persistence/criteria/Numeric.java index 18c6214..360f0cc 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/Numeric.java +++ b/src/main/java/org/omnifaces/persistence/criteria/Numeric.java @@ -29,54 +29,54 @@ */ public final class Numeric extends Criteria { - private Numeric(Number value) { - super(value); - } + private Numeric(Number value) { + super(value); + } - public static Numeric value(Number value) { - return new Numeric(value); - } + public static Numeric value(Number value) { + return new Numeric(value); + } - public static Numeric parse(Object searchValue, Class targetType) { - return new Numeric(parseNumber(searchValue, targetType)); - } + public static Numeric parse(Object searchValue, Class targetType) { + return new Numeric(parseNumber(searchValue, targetType)); + } - public static boolean is(Class type) { - return isOneOf(type, byte.class, short.class, int.class, long.class, float.class, double.class) || Number.class.isAssignableFrom(type); - } + public static boolean is(Class type) { + return isOneOf(type, byte.class, short.class, int.class, long.class, float.class, double.class) || Number.class.isAssignableFrom(type); + } - @Override - public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { - return criteriaBuilder.equal(path, parameterBuilder.create(getValue())); - } + @Override + public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { + return criteriaBuilder.equal(path, parameterBuilder.create(getValue())); + } - @Override - public boolean applies(Object modelValue) { - return modelValue != null && Objects.equals(parseNumber(modelValue, getValue().getClass()), getValue()); - } + @Override + public boolean applies(Object modelValue) { + return modelValue != null && Objects.equals(parseNumber(modelValue, getValue().getClass()), getValue()); + } - private static Number parseNumber(Object searchValue, Class targetType) throws NumberFormatException { - if (searchValue instanceof Number) { - return (Number) searchValue; - } + private static Number parseNumber(Object searchValue, Class targetType) throws NumberFormatException { + if (searchValue instanceof Number) { + return (Number) searchValue; + } - try { - if (BigDecimal.class.isAssignableFrom(targetType)) { - return new BigDecimal(searchValue.toString()); - } - else if (BigInteger.class.isAssignableFrom(targetType)) { - return new BigInteger(searchValue.toString()); - } - else if (Integer.class.isAssignableFrom(targetType)) { - return Integer.valueOf(searchValue.toString()); - } - else { - return Long.valueOf(searchValue.toString()); - } - } - catch (NumberFormatException e) { - throw new IllegalArgumentException(searchValue.toString(), e); - } - } + try { + if (BigDecimal.class.isAssignableFrom(targetType)) { + return new BigDecimal(searchValue.toString()); + } + else if (BigInteger.class.isAssignableFrom(targetType)) { + return new BigInteger(searchValue.toString()); + } + else if (Integer.class.isAssignableFrom(targetType)) { + return Integer.valueOf(searchValue.toString()); + } + else { + return Long.valueOf(searchValue.toString()); + } + } + catch (NumberFormatException e) { + throw new IllegalArgumentException(searchValue.toString(), e); + } + } } diff --git a/src/main/java/org/omnifaces/persistence/criteria/Order.java b/src/main/java/org/omnifaces/persistence/criteria/Order.java index 52f2efb..e2817b9 100644 --- a/src/main/java/org/omnifaces/persistence/criteria/Order.java +++ b/src/main/java/org/omnifaces/persistence/criteria/Order.java @@ -26,108 +26,108 @@ */ public final class Order> extends Criteria { - private enum Type { - LT, - LTE, - GT, - GTE - } - - private Type type; - - private Order(Type type, T value) { - super(value); - this.type = type; - } - - public static > Order lessThan(T value) { - return new Order<>(Type.LT, value); - } - - public static > Order lessThanOrEqualTo(T value) { - return new Order<>(Type.LTE, value); - } - - public static > Order greaterThanOrEqualTo(T value) { - return new Order<>(Type.GTE, value); - } - - public static > Order greaterThan(T value) { - return new Order<>(Type.GT, value); - } - - public boolean lessThan() { - return type == Type.LT; - } - - public boolean lessThanOrEqualTo() { - return type == Type.LTE; - } - - public boolean greaterThanOrEqualTo() { - return type == Type.GTE; - } - - public boolean greaterThan() { - return type == Type.GT; - } - - @Override - @SuppressWarnings("unchecked") - public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { - Expression typedPath = (Expression) path; - ParameterExpression parameter = parameterBuilder.create(getValue()); - - if (lessThan()) { - return criteriaBuilder.lessThan(typedPath, parameter); - } - else if (lessThanOrEqualTo()) { - return criteriaBuilder.lessThanOrEqualTo(typedPath, parameter); - } - else if (greaterThanOrEqualTo()) { - return criteriaBuilder.greaterThanOrEqualTo(typedPath, parameter); - } - else { - return criteriaBuilder.greaterThan(typedPath, parameter); - } - } - - @Override - @SuppressWarnings("unchecked") - public boolean applies(Object value) { - if (!(value instanceof Comparable)) { - return false; - } - - T typedValue = (T) value; - - if (greaterThan()) { - return typedValue.compareTo(getValue()) > 0; - } - else if (greaterThanOrEqualTo()) { - return typedValue.compareTo(getValue()) >= 0; - } - else if (lessThan()) { - return typedValue.compareTo(getValue()) < 0; - } - else { - return typedValue.compareTo(getValue()) <= 0; - } - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), type); - } - - @Override - public boolean equals(Object object) { - return super.equals(object) && Objects.equals(type, ((Order) object).type); - } - - @Override - public String toString() { - return (type == Type.GT ? ">" : type == Type.GTE ? ">=" : type==Type.LT ? "<" : "<=") + " " + getValue(); - } + private enum Type { + LT, + LTE, + GT, + GTE + } + + private Type type; + + private Order(Type type, T value) { + super(value); + this.type = type; + } + + public static > Order lessThan(T value) { + return new Order<>(Type.LT, value); + } + + public static > Order lessThanOrEqualTo(T value) { + return new Order<>(Type.LTE, value); + } + + public static > Order greaterThanOrEqualTo(T value) { + return new Order<>(Type.GTE, value); + } + + public static > Order greaterThan(T value) { + return new Order<>(Type.GT, value); + } + + public boolean lessThan() { + return type == Type.LT; + } + + public boolean lessThanOrEqualTo() { + return type == Type.LTE; + } + + public boolean greaterThanOrEqualTo() { + return type == Type.GTE; + } + + public boolean greaterThan() { + return type == Type.GT; + } + + @Override + @SuppressWarnings("unchecked") + public Predicate build(Expression path, CriteriaBuilder criteriaBuilder, ParameterBuilder parameterBuilder) { + Expression typedPath = (Expression) path; + ParameterExpression parameter = parameterBuilder.create(getValue()); + + if (lessThan()) { + return criteriaBuilder.lessThan(typedPath, parameter); + } + else if (lessThanOrEqualTo()) { + return criteriaBuilder.lessThanOrEqualTo(typedPath, parameter); + } + else if (greaterThanOrEqualTo()) { + return criteriaBuilder.greaterThanOrEqualTo(typedPath, parameter); + } + else { + return criteriaBuilder.greaterThan(typedPath, parameter); + } + } + + @Override + @SuppressWarnings("unchecked") + public boolean applies(Object value) { + if (!(value instanceof Comparable)) { + return false; + } + + T typedValue = (T) value; + + if (greaterThan()) { + return typedValue.compareTo(getValue()) > 0; + } + else if (greaterThanOrEqualTo()) { + return typedValue.compareTo(getValue()) >= 0; + } + else if (lessThan()) { + return typedValue.compareTo(getValue()) < 0; + } + else { + return typedValue.compareTo(getValue()) <= 0; + } + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), type); + } + + @Override + public boolean equals(Object object) { + return super.equals(object) && Objects.equals(type, ((Order) object).type); + } + + @Override + public String toString() { + return (type == Type.GT ? ">" : type == Type.GTE ? ">=" : type==Type.LT ? "<" : "<=") + " " + getValue(); + } } diff --git a/src/main/java/org/omnifaces/persistence/datasource/CommonDataSourceWrapper.java b/src/main/java/org/omnifaces/persistence/datasource/CommonDataSourceWrapper.java index 1031e4b..28b5844 100644 --- a/src/main/java/org/omnifaces/persistence/datasource/CommonDataSourceWrapper.java +++ b/src/main/java/org/omnifaces/persistence/datasource/CommonDataSourceWrapper.java @@ -33,269 +33,269 @@ public class CommonDataSourceWrapper implements CommonDataSource { - private CommonDataSource commonDataSource; - private Map dataSourceProperties; - private Set commonProperties = new HashSet<>(asList( - "serverName", "databaseName", "portNumber", - "user", "password", "compatible", "logLevel", - "protocolVersion", "prepareThreshold", "receiveBufferSize", - "unknownLength", "socketTimeout", "ssl", "sslfactory", - "applicationName", "tcpKeepAlive", "binaryTransfer", - "binaryTransferEnable", "binaryTransferDisable" - )); - - public void initDataSource(CommonDataSource dataSource) { - this.commonDataSource = dataSource; - - try { - Map mutableProperties = new HashMap<>(); - for (PropertyDescriptor propertyDescriptor : getBeanInfo(dataSource.getClass()).getPropertyDescriptors()) { - mutableProperties.put(propertyDescriptor.getName(), propertyDescriptor); - } - - dataSourceProperties = unmodifiableMap(mutableProperties); - - } catch (IntrospectionException e) { - throw new IllegalStateException(e); - } - } - - @SuppressWarnings("unchecked") - public T get(String name) { - - PropertyDescriptor property = dataSourceProperties.get(name); - - if ((property == null || property.getReadMethod() == null) && commonProperties.contains(name)) { - // Ignore fabricated properties that the actual data source doesn't have. - return null; - } - - try { - return (T) property.getReadMethod().invoke(commonDataSource); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new IllegalStateException(e); - } - } - - public void set(String name, Object value) { - - PropertyDescriptor property = dataSourceProperties.get(name); + private CommonDataSource commonDataSource; + private Map dataSourceProperties; + private Set commonProperties = new HashSet<>(asList( + "serverName", "databaseName", "portNumber", + "user", "password", "compatible", "logLevel", + "protocolVersion", "prepareThreshold", "receiveBufferSize", + "unknownLength", "socketTimeout", "ssl", "sslfactory", + "applicationName", "tcpKeepAlive", "binaryTransfer", + "binaryTransferEnable", "binaryTransferDisable" + )); + + public void initDataSource(CommonDataSource dataSource) { + this.commonDataSource = dataSource; + + try { + Map mutableProperties = new HashMap<>(); + for (PropertyDescriptor propertyDescriptor : getBeanInfo(dataSource.getClass()).getPropertyDescriptors()) { + mutableProperties.put(propertyDescriptor.getName(), propertyDescriptor); + } + + dataSourceProperties = unmodifiableMap(mutableProperties); + + } catch (IntrospectionException e) { + throw new IllegalStateException(e); + } + } + + @SuppressWarnings("unchecked") + public T get(String name) { + + PropertyDescriptor property = dataSourceProperties.get(name); + + if ((property == null || property.getReadMethod() == null) && commonProperties.contains(name)) { + // Ignore fabricated properties that the actual data source doesn't have. + return null; + } + + try { + return (T) property.getReadMethod().invoke(commonDataSource); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + public void set(String name, Object value) { + + PropertyDescriptor property = dataSourceProperties.get(name); if ((property == null || property.getReadMethod() == null) && commonProperties.contains(name)) { // Ignore fabricated properties that the actual data source doesn't have. return; } - - try { - dataSourceProperties.get(name).getWriteMethod().invoke(commonDataSource, value); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new IllegalStateException(e); - } - } - - public void setWithConversion(String name, String value) { + + try { + dataSourceProperties.get(name).getWriteMethod().invoke(commonDataSource, value); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + public void setWithConversion(String name, String value) { + + PropertyDescriptor property = dataSourceProperties.get(name); + + PropertyEditor editor = findEditor(property.getPropertyType()); + editor.setAsText(value); + + try { + property.getWriteMethod().invoke(commonDataSource, editor.getValue()); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + public CommonDataSource getWrapped() { + return commonDataSource; + } + + // ------------------------- CommonDataSource----------------------------------- + + @Override + public java.io.PrintWriter getLogWriter() throws SQLException { + return get("loginWriter"); + } + + @Override + public void setLogWriter(java.io.PrintWriter out) throws SQLException { + set("loginWriter", out); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + set("loginTimeout", seconds); + } + + @Override + public int getLoginTimeout() throws SQLException { + return get("loginTimeout"); + } + + // ------------------------- CommonDataSource JDBC 4.1 ----------------------------------- + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return commonDataSource.getParentLogger(); + } + + // ------------------------- Common properties ----------------------------------- + + public String getServerName() { + return get("serverName"); + } + + public void setServerName(String serverName) { + set("serverName", serverName); + } + + public String getDatabaseName() { + return get("databaseName"); + } - PropertyDescriptor property = dataSourceProperties.get(name); - - PropertyEditor editor = findEditor(property.getPropertyType()); - editor.setAsText(value); + public void setDatabaseName(String databaseName) { + set("databaseName", databaseName); + } + + public int getPortNumber() { + return get("portNumber"); + } + + public void setPortNumber(int portNumber) { + set("portNumber", portNumber); + } + + public void setPortNumber(Integer portNumber) { + set("portNumber", portNumber); + } + + public String getUser() { + return get("user"); + } + + public void setUser(String user) { + set("user", user); + } - try { - property.getWriteMethod().invoke(commonDataSource, editor.getValue()); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new IllegalStateException(e); - } - } + public String getPassword() { + return get("password"); + } + + public void setPassword(String password) { + set("password", password); + } - public CommonDataSource getWrapped() { - return commonDataSource; - } - - // ------------------------- CommonDataSource----------------------------------- - - @Override - public java.io.PrintWriter getLogWriter() throws SQLException { - return get("loginWriter"); - } - - @Override - public void setLogWriter(java.io.PrintWriter out) throws SQLException { - set("loginWriter", out); - } - - @Override - public void setLoginTimeout(int seconds) throws SQLException { - set("loginTimeout", seconds); - } - - @Override - public int getLoginTimeout() throws SQLException { - return get("loginTimeout"); - } - - // ------------------------- CommonDataSource JDBC 4.1 ----------------------------------- - - @Override - public Logger getParentLogger() throws SQLFeatureNotSupportedException { - return commonDataSource.getParentLogger(); - } - - // ------------------------- Common properties ----------------------------------- - - public String getServerName() { - return get("serverName"); - } - - public void setServerName(String serverName) { - set("serverName", serverName); - } - - public String getDatabaseName() { - return get("databaseName"); - } - - public void setDatabaseName(String databaseName) { - set("databaseName", databaseName); - } - - public int getPortNumber() { - return get("portNumber"); - } - - public void setPortNumber(int portNumber) { - set("portNumber", portNumber); - } - - public void setPortNumber(Integer portNumber) { - set("portNumber", portNumber); - } - - public String getUser() { - return get("user"); - } - - public void setUser(String user) { - set("user", user); - } - - public String getPassword() { - return get("password"); - } - - public void setPassword(String password) { - set("password", password); - } - - public String getCompatible() { - return get("compatible"); - } + public String getCompatible() { + return get("compatible"); + } - public void setCompatible(String compatible) { - set("compatible", compatible); - } + public void setCompatible(String compatible) { + set("compatible", compatible); + } - public int getLogLevel() { - return get("logLevel"); - } - - public void setLogLevel(int logLevel) { - set("logLevel", logLevel); - } - - public int getProtocolVersion() { - return get("protocolVersion"); - } + public int getLogLevel() { + return get("logLevel"); + } - public void setProtocolVersion(int protocolVersion) { - set("protocolVersion", protocolVersion); - } - - public int getPrepareThreshold() { + public void setLogLevel(int logLevel) { + set("logLevel", logLevel); + } + + public int getProtocolVersion() { + return get("protocolVersion"); + } + + public void setProtocolVersion(int protocolVersion) { + set("protocolVersion", protocolVersion); + } + + public int getPrepareThreshold() { return get("prepareThreshold"); } - public void setPrepareThreshold(int prepareThreshold) { - set("prepareThreshold", prepareThreshold); - } + public void setPrepareThreshold(int prepareThreshold) { + set("prepareThreshold", prepareThreshold); + } - public void setReceiveBufferSize(int receiveBufferSize) { - set("receiveBufferSize", receiveBufferSize); - } + public void setReceiveBufferSize(int receiveBufferSize) { + set("receiveBufferSize", receiveBufferSize); + } - public void setSendBufferSize(int sendBufferSize) { - set("sendBufferSize", sendBufferSize); - } + public void setSendBufferSize(int sendBufferSize) { + set("sendBufferSize", sendBufferSize); + } - public void setUnknownLength(int unknownLength) { - set("unknownLength", unknownLength); - } + public void setUnknownLength(int unknownLength) { + set("unknownLength", unknownLength); + } - public int getUnknownLength() { - return get("unknownLength"); - } + public int getUnknownLength() { + return get("unknownLength"); + } - public void setSocketTimeout(int socketTimeout) { - set("socketTimeout", socketTimeout); - } + public void setSocketTimeout(int socketTimeout) { + set("socketTimeout", socketTimeout); + } - public int getSocketTimeout() { - return get("socketTimeout"); - } + public int getSocketTimeout() { + return get("socketTimeout"); + } - public void setSsl(boolean ssl) { - set("ssl", ssl); - } + public void setSsl(boolean ssl) { + set("ssl", ssl); + } - public boolean getSsl() { - return get("ssl"); - } + public boolean getSsl() { + return get("ssl"); + } - public void setSslfactory(String sslfactory) { - set("sslfactory", sslfactory); - } + public void setSslfactory(String sslfactory) { + set("sslfactory", sslfactory); + } - public String getSslfactory() { - return get("sslfactory"); - } + public String getSslfactory() { + return get("sslfactory"); + } - public void setApplicationName(String applicationName) { - set("applicationName", applicationName); - } + public void setApplicationName(String applicationName) { + set("applicationName", applicationName); + } - public String getApplicationName() { - return get("applicationName"); - } + public String getApplicationName() { + return get("applicationName"); + } - public void setTcpKeepAlive(boolean tcpKeepAlive) { - set("tcpKeepAlive", tcpKeepAlive); - } + public void setTcpKeepAlive(boolean tcpKeepAlive) { + set("tcpKeepAlive", tcpKeepAlive); + } - public boolean getTcpKeepAlive() { - return get("tcpKeepAlive"); - } + public boolean getTcpKeepAlive() { + return get("tcpKeepAlive"); + } - public void setBinaryTransfer(boolean binaryTransfer) { - set("binaryTransfer", binaryTransfer); - } + public void setBinaryTransfer(boolean binaryTransfer) { + set("binaryTransfer", binaryTransfer); + } - public boolean getBinaryTransfer() { - return get("binaryTransfer"); - } + public boolean getBinaryTransfer() { + return get("binaryTransfer"); + } - public void setBinaryTransferEnable(String binaryTransferEnable) { - set("binaryTransferEnable", binaryTransferEnable); - } + public void setBinaryTransferEnable(String binaryTransferEnable) { + set("binaryTransferEnable", binaryTransferEnable); + } - public String getBinaryTransferEnable() { - return get("binaryTransferEnable"); - } + public String getBinaryTransferEnable() { + return get("binaryTransferEnable"); + } - public void setBinaryTransferDisable(String binaryTransferDisable) { - set("binaryTransferDisable", binaryTransferDisable); - } + public void setBinaryTransferDisable(String binaryTransferDisable) { + set("binaryTransferDisable", binaryTransferDisable); + } - public String getBinaryTransferDisable() { - return get("binaryTransferDisable"); - } + public String getBinaryTransferDisable() { + return get("binaryTransferDisable"); + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/datasource/PropertiesFileLoader.java b/src/main/java/org/omnifaces/persistence/datasource/PropertiesFileLoader.java index 881a215..ed715a3 100644 --- a/src/main/java/org/omnifaces/persistence/datasource/PropertiesFileLoader.java +++ b/src/main/java/org/omnifaces/persistence/datasource/PropertiesFileLoader.java @@ -16,6 +16,6 @@ public interface PropertiesFileLoader { - Map loadFromFile(String fileName); - + Map loadFromFile(String fileName); + } diff --git a/src/main/java/org/omnifaces/persistence/datasource/SwitchableCommonDataSource.java b/src/main/java/org/omnifaces/persistence/datasource/SwitchableCommonDataSource.java index 34d7e9f..388da82 100644 --- a/src/main/java/org/omnifaces/persistence/datasource/SwitchableCommonDataSource.java +++ b/src/main/java/org/omnifaces/persistence/datasource/SwitchableCommonDataSource.java @@ -22,86 +22,86 @@ public class SwitchableCommonDataSource extends CommonDataSourceWrapper { - private boolean init; - private String configFile; - private Map tempValues = new HashMap<>(); - - @Override - public void set(String name, Object value) { - if (init) { - super.set(name, value); - } else { - tempValues.put(name, value); - } - } - - @SuppressWarnings("unchecked") - @Override - public T get(String name) { - if (init) { - return super.get(name); - } else { - return (T) tempValues.get(name); - } - } - - public String getConfigFile() { - return configFile; - } - - public void setConfigFile(String configFile) { - this.configFile = configFile; - - // Nasty, but there's not an @PostConstruct equivalent on a DataSource that's called - // when all properties have been set. - doInit(); - } - - public void doInit() { - - // Get the properties that were defined separately from the @DataSourceDefinition/data-source element - - ServiceLoader loader = ServiceLoader.load(PropertiesFileLoader.class); - if (!loader.iterator().hasNext()) { - loader = ServiceLoader.load(PropertiesFileLoader.class, SwitchableCommonDataSource.class.getClassLoader()); - } - - Map properties = new HashMap<>(); - - if (!loader.iterator().hasNext()) { - // No service loader was specified for loading the configfile. - // Try the fallback default location of META-INF on the classpath - properties.putAll(loadPropertiesFromClasspath("META-INF/" + configFile)); - - } else { - for (PropertiesFileLoader propertiesFileLoader : loader) { - properties.putAll(propertiesFileLoader.loadFromFile(configFile)); - } - } - - // Get & check the most important property; the class name of the data source that we wrap. - String className = properties.get("className"); - if (className == null) { - throw new IllegalStateException("Required parameter 'className' missing."); - } - - initDataSource(instantiate(className)); - - // Set the properties on the wrapped data source that were already set on this class before doInit() - // was possible. - for (Entry property : tempValues.entrySet()) { - super.set(property.getKey(), property.getValue()); - } - - // Set the properties on the wrapped data source that were loaded from the external file. - for (Entry property : properties.entrySet()) { - if (!property.getKey().equals("className")) { - setWithConversion(property.getKey(), property.getValue()); - } - } - - // After this properties will be set directly on the wrapped data source instance. - init = true; - } + private boolean init; + private String configFile; + private Map tempValues = new HashMap<>(); + + @Override + public void set(String name, Object value) { + if (init) { + super.set(name, value); + } else { + tempValues.put(name, value); + } + } + + @SuppressWarnings("unchecked") + @Override + public T get(String name) { + if (init) { + return super.get(name); + } else { + return (T) tempValues.get(name); + } + } + + public String getConfigFile() { + return configFile; + } + + public void setConfigFile(String configFile) { + this.configFile = configFile; + + // Nasty, but there's not an @PostConstruct equivalent on a DataSource that's called + // when all properties have been set. + doInit(); + } + + public void doInit() { + + // Get the properties that were defined separately from the @DataSourceDefinition/data-source element + + ServiceLoader loader = ServiceLoader.load(PropertiesFileLoader.class); + if (!loader.iterator().hasNext()) { + loader = ServiceLoader.load(PropertiesFileLoader.class, SwitchableCommonDataSource.class.getClassLoader()); + } + + Map properties = new HashMap<>(); + + if (!loader.iterator().hasNext()) { + // No service loader was specified for loading the configfile. + // Try the fallback default location of META-INF on the classpath + properties.putAll(loadPropertiesFromClasspath("META-INF/" + configFile)); + + } else { + for (PropertiesFileLoader propertiesFileLoader : loader) { + properties.putAll(propertiesFileLoader.loadFromFile(configFile)); + } + } + + // Get & check the most important property; the class name of the data source that we wrap. + String className = properties.get("className"); + if (className == null) { + throw new IllegalStateException("Required parameter 'className' missing."); + } + + initDataSource(instantiate(className)); + + // Set the properties on the wrapped data source that were already set on this class before doInit() + // was possible. + for (Entry property : tempValues.entrySet()) { + super.set(property.getKey(), property.getValue()); + } + + // Set the properties on the wrapped data source that were loaded from the external file. + for (Entry property : properties.entrySet()) { + if (!property.getKey().equals("className")) { + setWithConversion(property.getKey(), property.getValue()); + } + } + + // After this properties will be set directly on the wrapped data source instance. + init = true; + } } diff --git a/src/main/java/org/omnifaces/persistence/datasource/SwitchableXADataSource.java b/src/main/java/org/omnifaces/persistence/datasource/SwitchableXADataSource.java index 62d04b4..9115c33 100644 --- a/src/main/java/org/omnifaces/persistence/datasource/SwitchableXADataSource.java +++ b/src/main/java/org/omnifaces/persistence/datasource/SwitchableXADataSource.java @@ -19,21 +19,21 @@ public class SwitchableXADataSource extends SwitchableCommonDataSource implements XADataSource { - @Override - public XADataSource getWrapped() { - return (XADataSource) super.getWrapped(); - } - - // ------------------------- XADataSource----------------------------------- - - @Override - public XAConnection getXAConnection() throws SQLException { - return getWrapped().getXAConnection(); - } - - @Override - public XAConnection getXAConnection(String user, String password) throws SQLException { - return getWrapped().getXAConnection(); - } + @Override + public XADataSource getWrapped() { + return (XADataSource) super.getWrapped(); + } + + // ------------------------- XADataSource----------------------------------- + + @Override + public XAConnection getXAConnection() throws SQLException { + return getWrapped().getXAConnection(); + } + + @Override + public XAConnection getXAConnection(String user, String password) throws SQLException { + return getWrapped().getXAConnection(); + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/event/Created.java b/src/main/java/org/omnifaces/persistence/event/Created.java index 221b7e7..3e427e7 100644 --- a/src/main/java/org/omnifaces/persistence/event/Created.java +++ b/src/main/java/org/omnifaces/persistence/event/Created.java @@ -30,5 +30,5 @@ @Retention(RUNTIME) @Target(PARAMETER) public @interface Created { - // + // } diff --git a/src/main/java/org/omnifaces/persistence/event/Deleted.java b/src/main/java/org/omnifaces/persistence/event/Deleted.java index 6f9f824..c1f6195 100644 --- a/src/main/java/org/omnifaces/persistence/event/Deleted.java +++ b/src/main/java/org/omnifaces/persistence/event/Deleted.java @@ -30,5 +30,5 @@ @Retention(RUNTIME) @Target(PARAMETER) public @interface Deleted { - // + // } diff --git a/src/main/java/org/omnifaces/persistence/event/Updated.java b/src/main/java/org/omnifaces/persistence/event/Updated.java index ebc89b2..688144f 100644 --- a/src/main/java/org/omnifaces/persistence/event/Updated.java +++ b/src/main/java/org/omnifaces/persistence/event/Updated.java @@ -30,5 +30,5 @@ @Retention(RUNTIME) @Target(PARAMETER) public @interface Updated { - // + // } diff --git a/src/main/java/org/omnifaces/persistence/exception/BaseEntityException.java b/src/main/java/org/omnifaces/persistence/exception/BaseEntityException.java index 5a71fc9..f936c88 100644 --- a/src/main/java/org/omnifaces/persistence/exception/BaseEntityException.java +++ b/src/main/java/org/omnifaces/persistence/exception/BaseEntityException.java @@ -20,18 +20,18 @@ @ApplicationException(rollback = true) public abstract class BaseEntityException extends PersistenceException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private BaseEntity entity; + private BaseEntity entity; - public BaseEntityException(BaseEntity entity, String message) { - super(message); - this.entity = entity; - } + public BaseEntityException(BaseEntity entity, String message) { + super(message); + this.entity = entity; + } - @SuppressWarnings("unchecked") - public > E getEntity() { - return (E) entity; - } + @SuppressWarnings("unchecked") + public > E getEntity() { + return (E) entity; + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/exception/IllegalEntityStateException.java b/src/main/java/org/omnifaces/persistence/exception/IllegalEntityStateException.java index 351937e..a1a1032 100644 --- a/src/main/java/org/omnifaces/persistence/exception/IllegalEntityStateException.java +++ b/src/main/java/org/omnifaces/persistence/exception/IllegalEntityStateException.java @@ -16,10 +16,10 @@ public class IllegalEntityStateException extends BaseEntityException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public IllegalEntityStateException(BaseEntity entity, String message) { - super(entity, message); - } + public IllegalEntityStateException(BaseEntity entity, String message) { + super(entity, message); + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/exception/NonDeletableEntityException.java b/src/main/java/org/omnifaces/persistence/exception/NonDeletableEntityException.java index e22210b..7cb9c57 100644 --- a/src/main/java/org/omnifaces/persistence/exception/NonDeletableEntityException.java +++ b/src/main/java/org/omnifaces/persistence/exception/NonDeletableEntityException.java @@ -16,10 +16,10 @@ public class NonDeletableEntityException extends BaseEntityException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public NonDeletableEntityException(BaseEntity entity) { - super(entity, null); - } + public NonDeletableEntityException(BaseEntity entity) { + super(entity, null); + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/exception/NonSoftDeletableEntityException.java b/src/main/java/org/omnifaces/persistence/exception/NonSoftDeletableEntityException.java index 9ed68ef..1afb7a1 100644 --- a/src/main/java/org/omnifaces/persistence/exception/NonSoftDeletableEntityException.java +++ b/src/main/java/org/omnifaces/persistence/exception/NonSoftDeletableEntityException.java @@ -16,10 +16,10 @@ public class NonSoftDeletableEntityException extends BaseEntityException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public NonSoftDeletableEntityException(BaseEntity entity, String message) { - super(entity, message); - } + public NonSoftDeletableEntityException(BaseEntity entity, String message) { + super(entity, message); + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/listener/BaseEntityListener.java b/src/main/java/org/omnifaces/persistence/listener/BaseEntityListener.java index ce1c4f2..c64835a 100644 --- a/src/main/java/org/omnifaces/persistence/listener/BaseEntityListener.java +++ b/src/main/java/org/omnifaces/persistence/listener/BaseEntityListener.java @@ -56,52 +56,52 @@ */ public class BaseEntityListener { - @Inject - private BeanManager beanManager; - - private Optional optionalBeanManager; - - @PostPersist - public void onPostPersist(BaseEntity entity) { - fireOptionalEvent(entity, Created.class); - } - - @PostUpdate - public void onPostUpdate(BaseEntity entity) { - fireOptionalEvent(entity, Updated.class); - } - - @PostRemove - public void onPostRemove(BaseEntity entity) { - fireOptionalEvent(entity, Deleted.class); - } - - private BeanManager getBeanManager() { - if (beanManager == null) { - try { - beanManager = CDI.current().getBeanManager(); // Work around for CDI inject not working in JPA EntityListener (as observed in OpenJPA). - } - catch (IllegalStateException ignore) { - beanManager = null; // Can happen when actually not in CDI environment, e.g. local unit test. - } - } - - return beanManager; - } - - private Optional getOptionalBeanManager() { - if (optionalBeanManager == null) { - optionalBeanManager = Optional.ofNullable(getBeanManager()); - } - - return optionalBeanManager; - } - - private void fireOptionalEvent(BaseEntity entity, Class eventType) { - getOptionalBeanManager().ifPresent(beanManager -> - beanManager.getEvent() - .select(createAnnotationInstance(eventType)) - .fire(entity)); - } + @Inject + private BeanManager beanManager; + + private Optional optionalBeanManager; + + @PostPersist + public void onPostPersist(BaseEntity entity) { + fireOptionalEvent(entity, Created.class); + } + + @PostUpdate + public void onPostUpdate(BaseEntity entity) { + fireOptionalEvent(entity, Updated.class); + } + + @PostRemove + public void onPostRemove(BaseEntity entity) { + fireOptionalEvent(entity, Deleted.class); + } + + private BeanManager getBeanManager() { + if (beanManager == null) { + try { + beanManager = CDI.current().getBeanManager(); // Work around for CDI inject not working in JPA EntityListener (as observed in OpenJPA). + } + catch (IllegalStateException ignore) { + beanManager = null; // Can happen when actually not in CDI environment, e.g. local unit test. + } + } + + return beanManager; + } + + private Optional getOptionalBeanManager() { + if (optionalBeanManager == null) { + optionalBeanManager = Optional.ofNullable(getBeanManager()); + } + + return optionalBeanManager; + } + + private void fireOptionalEvent(BaseEntity entity, Class eventType) { + getOptionalBeanManager().ifPresent(beanManager -> + beanManager.getEvent() + .select(createAnnotationInstance(eventType)) + .fire(entity)); + } } diff --git a/src/main/java/org/omnifaces/persistence/model/BaseEntity.java b/src/main/java/org/omnifaces/persistence/model/BaseEntity.java index 9eb5999..83ed0d4 100644 --- a/src/main/java/org/omnifaces/persistence/model/BaseEntity.java +++ b/src/main/java/org/omnifaces/persistence/model/BaseEntity.java @@ -42,46 +42,46 @@ @EntityListeners(BaseEntityListener.class) public abstract class BaseEntity & Serializable> implements Comparable>, Identifiable, Serializable { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - /** - * Hashes by default the ID. - */ - @Override + /** + * Hashes by default the ID. + */ + @Override public int hashCode() { return (getId() != null) - ? Objects.hash(getId()) - : super.hashCode(); + ? Objects.hash(getId()) + : super.hashCode(); } - /** - * Compares by default by entity class (proxies taken into account) and ID. - */ - @Override + /** + * Compares by default by entity class (proxies taken into account) and ID. + */ + @Override public boolean equals(Object other) { return (getId() != null && getClass().isInstance(other) && other.getClass().isInstance(this)) ? getId().equals(((BaseEntity) other).getId()) : (other == this); } - /** - * Orders by default with "nulls last". - */ - @Override - public int compareTo(BaseEntity other) { - return (other == null) - ? -1 - : (getId() == null) - ? (other.getId() == null ? 0 : 1) - : getId().compareTo(other.getId()); - } + /** + * Orders by default with "nulls last". + */ + @Override + public int compareTo(BaseEntity other) { + return (other == null) + ? -1 + : (getId() == null) + ? (other.getId() == null ? 0 : 1) + : getId().compareTo(other.getId()); + } - /** - * The default format is ClassName[{id}] where {id} defaults to @hashcode when null. - */ - @Override - public String toString() { - return String.format("%s[%s]", getClass().getSimpleName(), (getId() != null) ? getId() : ("@" + hashCode())); - } + /** + * The default format is ClassName[{id}] where {id} defaults to @hashcode when null. + */ + @Override + public String toString() { + return String.format("%s[%s]", getClass().getSimpleName(), (getId() != null) ? getId() : ("@" + hashCode())); + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/model/GeneratedIdEntity.java b/src/main/java/org/omnifaces/persistence/model/GeneratedIdEntity.java index fdf5127..5dc45aa 100644 --- a/src/main/java/org/omnifaces/persistence/model/GeneratedIdEntity.java +++ b/src/main/java/org/omnifaces/persistence/model/GeneratedIdEntity.java @@ -33,19 +33,19 @@ @MappedSuperclass public abstract class GeneratedIdEntity & Serializable> extends BaseEntity { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @Id @GeneratedValue(strategy = IDENTITY) - private I id; + @Id @GeneratedValue(strategy = IDENTITY) + private I id; - @Override - public I getId() { - return id; - } + @Override + public I getId() { + return id; + } - @Override - public void setId(I id) { - this.id = id; - } + @Override + public void setId(I id) { + this.id = id; + } } diff --git a/src/main/java/org/omnifaces/persistence/model/Identifiable.java b/src/main/java/org/omnifaces/persistence/model/Identifiable.java index 3473822..1ce91b4 100644 --- a/src/main/java/org/omnifaces/persistence/model/Identifiable.java +++ b/src/main/java/org/omnifaces/persistence/model/Identifiable.java @@ -23,21 +23,21 @@ */ public interface Identifiable & Serializable> { - /** - * The string representing the field name "id". - */ - String ID = "id"; + /** + * The string representing the field name "id". + */ + String ID = "id"; - /** - * Returns the ID. - * @return The ID. - */ - I getId(); + /** + * Returns the ID. + * @return The ID. + */ + I getId(); - /** - * Sets the ID. - * @param id The ID. - */ - void setId(I id); + /** + * Sets the ID. + * @param id The ID. + */ + void setId(I id); } diff --git a/src/main/java/org/omnifaces/persistence/model/NonDeletable.java b/src/main/java/org/omnifaces/persistence/model/NonDeletable.java index c03e9a6..27e621a 100644 --- a/src/main/java/org/omnifaces/persistence/model/NonDeletable.java +++ b/src/main/java/org/omnifaces/persistence/model/NonDeletable.java @@ -29,5 +29,5 @@ @Target(TYPE) @Retention(RUNTIME) public @interface NonDeletable { - // + // } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/model/SoftDeletable.java b/src/main/java/org/omnifaces/persistence/model/SoftDeletable.java index d7bce08..1464c45 100644 --- a/src/main/java/org/omnifaces/persistence/model/SoftDeletable.java +++ b/src/main/java/org/omnifaces/persistence/model/SoftDeletable.java @@ -37,32 +37,32 @@ @Retention(RUNTIME) public @interface SoftDeletable { - /** - * Defines the types of the soft delete column. - * @author Sergey Kuntsel - */ - public enum Type { + /** + * Defines the types of the soft delete column. + * @author Sergey Kuntsel + */ + public enum Type { - /** - * Indicates that the associated column is a column holding deleted state. - * All entities that haven't been soft deleted will thus have false - * in the soft delete column, assuming it was mapped as boolean. - * This is the default type. - */ - DELETED, + /** + * Indicates that the associated column is a column holding deleted state. + * All entities that haven't been soft deleted will thus have false + * in the soft delete column, assuming it was mapped as boolean. + * This is the default type. + */ + DELETED, - /** - * Indicates that the associated column is a column holding active state. - * All entities that haven't been soft deleted will thus have true - * in the soft delete column, assuming it was mapped as boolean. - */ - ACTIVE - } + /** + * Indicates that the associated column is a column holding active state. + * All entities that haven't been soft deleted will thus have true + * in the soft delete column, assuming it was mapped as boolean. + */ + ACTIVE + } - /** - * Returns The soft deletable type. Defaults to {@link Type#DELETED}. - * @return The soft deletable type. - */ - public Type type() default Type.DELETED; + /** + * Returns The soft deletable type. Defaults to {@link Type#DELETED}. + * @return The soft deletable type. + */ + public Type type() default Type.DELETED; } diff --git a/src/main/java/org/omnifaces/persistence/model/Timestamped.java b/src/main/java/org/omnifaces/persistence/model/Timestamped.java index 3bc25f3..2d6378c 100644 --- a/src/main/java/org/omnifaces/persistence/model/Timestamped.java +++ b/src/main/java/org/omnifaces/persistence/model/Timestamped.java @@ -22,10 +22,10 @@ */ public interface Timestamped { - void setCreated(Instant created); - Instant getCreated(); + void setCreated(Instant created); + Instant getCreated(); - void setLastModified(Instant lastModified); - Instant getLastModified(); + void setLastModified(Instant lastModified); + Instant getLastModified(); } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/model/TimestampedBaseEntity.java b/src/main/java/org/omnifaces/persistence/model/TimestampedBaseEntity.java index bf9ed4e..f456aeb 100644 --- a/src/main/java/org/omnifaces/persistence/model/TimestampedBaseEntity.java +++ b/src/main/java/org/omnifaces/persistence/model/TimestampedBaseEntity.java @@ -39,58 +39,58 @@ @MappedSuperclass public abstract class TimestampedBaseEntity & Serializable> extends BaseEntity implements Timestamped { - private static final long serialVersionUID = 1L; - - @Column(nullable = false) - private Instant created; - - @Column(nullable = false) - private Instant lastModified; - - @Transient - private boolean skipAdjustLastModified; - - @PrePersist - public void onPrePersist() { - Instant timestamp = now(); - setCreated(timestamp); - setLastModified(timestamp); - } - - @PreUpdate - public void onPreUpdate() { - if (!skipAdjustLastModified) { - Instant timestamp = now(); - setLastModified(timestamp); - } - } - - /** - * Invoke this method if you need to skip adjusting the "last modified" timestamp during any update event on this - * instance. In case you intend to reset this later on, simply obtain a new instance from the entity manager. - */ - public void skipAdjustLastModified() { - this.skipAdjustLastModified = true; - } - - @Override - public void setCreated(Instant created) { - this.created = created; - } - - @Override - public Instant getCreated() { - return created; - } - - @Override - public void setLastModified(Instant lastModified) { - this.lastModified = lastModified; - } - - @Override - public Instant getLastModified() { - return lastModified; - } + private static final long serialVersionUID = 1L; + + @Column(nullable = false) + private Instant created; + + @Column(nullable = false) + private Instant lastModified; + + @Transient + private boolean skipAdjustLastModified; + + @PrePersist + public void onPrePersist() { + Instant timestamp = now(); + setCreated(timestamp); + setLastModified(timestamp); + } + + @PreUpdate + public void onPreUpdate() { + if (!skipAdjustLastModified) { + Instant timestamp = now(); + setLastModified(timestamp); + } + } + + /** + * Invoke this method if you need to skip adjusting the "last modified" timestamp during any update event on this + * instance. In case you intend to reset this later on, simply obtain a new instance from the entity manager. + */ + public void skipAdjustLastModified() { + this.skipAdjustLastModified = true; + } + + @Override + public void setCreated(Instant created) { + this.created = created; + } + + @Override + public Instant getCreated() { + return created; + } + + @Override + public void setLastModified(Instant lastModified) { + this.lastModified = lastModified; + } + + @Override + public Instant getLastModified() { + return lastModified; + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/model/TimestampedEntity.java b/src/main/java/org/omnifaces/persistence/model/TimestampedEntity.java index fb7dae9..08fa74f 100644 --- a/src/main/java/org/omnifaces/persistence/model/TimestampedEntity.java +++ b/src/main/java/org/omnifaces/persistence/model/TimestampedEntity.java @@ -37,58 +37,58 @@ @MappedSuperclass public abstract class TimestampedEntity & Serializable> extends GeneratedIdEntity implements Timestamped { - private static final long serialVersionUID = 1L; - - @Column(nullable = false) - private Instant created; - - @Column(nullable = false) - private Instant lastModified; - - @Transient - private boolean skipAdjustLastModified; - - @PrePersist - public void onPrePersist() { - Instant timestamp = now(); - setCreated(timestamp); - setLastModified(timestamp); - } - - @PreUpdate - public void onPreUpdate() { - if (!skipAdjustLastModified) { - Instant timestamp = now(); - setLastModified(timestamp); - } - } - - /** - * Invoke this method if you need to skip adjusting the "last modified" timestamp during any update event on this - * instance. In case you intend to reset this later on, simply obtain a new instance from the entity manager. - */ - public void skipAdjustLastModified() { - this.skipAdjustLastModified = true; - } - - @Override - public void setCreated(Instant created) { - this.created = created; - } - - @Override - public Instant getCreated() { - return created; - } - - @Override - public void setLastModified(Instant lastModified) { - this.lastModified = lastModified; - } - - @Override - public Instant getLastModified() { - return lastModified; - } + private static final long serialVersionUID = 1L; + + @Column(nullable = false) + private Instant created; + + @Column(nullable = false) + private Instant lastModified; + + @Transient + private boolean skipAdjustLastModified; + + @PrePersist + public void onPrePersist() { + Instant timestamp = now(); + setCreated(timestamp); + setLastModified(timestamp); + } + + @PreUpdate + public void onPreUpdate() { + if (!skipAdjustLastModified) { + Instant timestamp = now(); + setLastModified(timestamp); + } + } + + /** + * Invoke this method if you need to skip adjusting the "last modified" timestamp during any update event on this + * instance. In case you intend to reset this later on, simply obtain a new instance from the entity manager. + */ + public void skipAdjustLastModified() { + this.skipAdjustLastModified = true; + } + + @Override + public void setCreated(Instant created) { + this.created = created; + } + + @Override + public Instant getCreated() { + return created; + } + + @Override + public void setLastModified(Instant lastModified) { + this.lastModified = lastModified; + } + + @Override + public Instant getLastModified() { + return lastModified; + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/model/Versioned.java b/src/main/java/org/omnifaces/persistence/model/Versioned.java index 3ecd5f6..3ae06e1 100644 --- a/src/main/java/org/omnifaces/persistence/model/Versioned.java +++ b/src/main/java/org/omnifaces/persistence/model/Versioned.java @@ -20,6 +20,6 @@ */ public interface Versioned { - Long getVersion(); + Long getVersion(); } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/model/VersionedBaseEntity.java b/src/main/java/org/omnifaces/persistence/model/VersionedBaseEntity.java index 587d434..c7ccfbe 100644 --- a/src/main/java/org/omnifaces/persistence/model/VersionedBaseEntity.java +++ b/src/main/java/org/omnifaces/persistence/model/VersionedBaseEntity.java @@ -34,16 +34,16 @@ @MappedSuperclass public abstract class VersionedBaseEntity & Serializable> extends TimestampedBaseEntity implements Versioned { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @Version - @Column(nullable = false) - private Long version; + @Version + @Column(nullable = false) + private Long version; - @Override - public Long getVersion() { - return version; - } + @Override + public Long getVersion() { + return version; + } - // No setter! JPA takes care of this. + // No setter! JPA takes care of this. } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/model/VersionedEntity.java b/src/main/java/org/omnifaces/persistence/model/VersionedEntity.java index 875e407..981c952 100644 --- a/src/main/java/org/omnifaces/persistence/model/VersionedEntity.java +++ b/src/main/java/org/omnifaces/persistence/model/VersionedEntity.java @@ -32,16 +32,16 @@ @MappedSuperclass public abstract class VersionedEntity & Serializable> extends TimestampedEntity implements Versioned { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @Version - @Column(nullable = false) - private Long version; + @Version + @Column(nullable = false) + private Long version; - @Override - public Long getVersion() { - return version; - } + @Override + public Long getVersion() { + return version; + } - // No setter! JPA takes care of this. + // No setter! JPA takes care of this. } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/model/dto/Page.java b/src/main/java/org/omnifaces/persistence/model/dto/Page.java index f44546a..9d89f29 100644 --- a/src/main/java/org/omnifaces/persistence/model/dto/Page.java +++ b/src/main/java/org/omnifaces/persistence/model/dto/Page.java @@ -40,281 +40,281 @@ */ public final class Page { // This class MAY NOT be mutable! - // Constants ------------------------------------------------------------------------------------------------------ - - public final static Page ALL = Page.of(0, MAX_VALUE); - public final static Page ONE = Page.of(0, 1); - - - // Properties ----------------------------------------------------------------------------------------------------- - - private final int offset; - private final int limit; - private final Identifiable last; - private final boolean reversed; - private final Map ordering; - private final Map requiredCriteria; - private final Map optionalCriteria; - - - // Constructors --------------------------------------------------------------------------------------------------- - - /** - * Creates a new Page. You can for convenience also use {@link Page#of(int, int)} or the {@link Page#with()} builder. - * @param offset Zero-based offset of the page. May not be negative. Defaults to 0. - * @param limit Maximum amount of records to be matched. May not be less than 1. Defaults to {@link Integer#MAX_VALUE}. - * @param ordering Ordering of results. Map key represents property path and map value represents whether to sort ascending. Defaults to {"id",false}. - * @param requiredCriteria Required criteria. Map key represents property path and map value represents criteria. Each entity must match all of given criteria. - * @param optionalCriteria Optional criteria. Map key represents property path and map value represents criteria. Each entity must match at least one of given criteria. - */ - public Page(Integer offset, Integer limit, LinkedHashMap ordering, Map requiredCriteria, Map optionalCriteria) { - this(offset, limit, null, null, ordering, requiredCriteria, optionalCriteria); - } - - /** - * Creates a new Page whereby value based paging will be performed instead of offset based paging when applicable. - * Value based paging is not applicable when the result type is a DTO, or when the ordering contains an aggregated field. - * @param offset Zero-based offset of the page. May not be negative. Defaults to 0. - * @param limit Maximum amount of records to be matched. May not be less than 1. Defaults to {@link Integer#MAX_VALUE}. - * @param last Last entity of the previous page. When not null, then value based paging will be performed instead of offset based paging when applicable. - * @param reversed Whether value based paging is reversed. This is ignored when last entity is null. Defaults to false. - * @param ordering Ordering of results. Map key represents property path and map value represents whether to sort ascending. Defaults to {"id",false}. - * @param requiredCriteria Required criteria. Map key represents property path and map value represents criteria. Each entity must match all of given criteria. - * @param optionalCriteria Optional criteria. Map key represents property path and map value represents criteria. Each entity must match at least one of given criteria. - */ - public Page(Integer offset, Integer limit, Identifiable last, Boolean reversed, LinkedHashMap ordering, Map requiredCriteria, Map optionalCriteria) { - this.offset = validateIntegerArgument("offset", offset, 0, 0); - this.limit = validateIntegerArgument("limit", limit, 1, MAX_VALUE); - this.last = last; - this.reversed = (last != null) && (reversed == TRUE); - this.ordering = !isEmpty(ordering) ? unmodifiableMap(ordering) : singletonMap(ID, false); - this.requiredCriteria = requiredCriteria != null ? unmodifiableMap(requiredCriteria) : emptyMap(); - this.optionalCriteria = optionalCriteria != null ? unmodifiableMap(optionalCriteria) : emptyMap(); - } - - private static int validateIntegerArgument(String argumentName, Integer argumentValue, int minValue, int defaultValue) { - if (argumentValue == null) { - return defaultValue; - } - - if (argumentValue < minValue) { - throw new IllegalArgumentException("Argument '" + argumentName + "' may not be less than " + minValue); - } - - return argumentValue; - } - - - // Getters -------------------------------------------------------------------------------------------------------- - - /** - * Returns the offset. Defaults to 0. - * @return The offset. - */ - public int getOffset() { - return offset; - } - - /** - * Returns the limit. Defaults to {@link Integer#MAX_VALUE}. - * @return The limit. - */ - public int getLimit() { - return limit; - } - - /** - * Returns the last entity of the previous page, if any. - * If not null, then value based paging will be performed instead of offset based paging when applicable. - * @return The last entity of the previous page, if any. - */ - public Identifiable getLast() { - return last; - } - - /** - * Returns whether the value based paging is reversed. - * This is only used when {@link #getLast()} is not null. - * @return Whether the value based paging is reversed. - */ - public boolean isReversed() { - return reversed; - } - - /** - * Returns the ordering. Map key represents property path and map value represents whether to sort ascending. Defaults to {"id",false}. - * @return The ordering. - */ - public Map getOrdering() { - return ordering; - } - - /** - * Returns the required criteria. Map key represents property path and map value represents criteria. Each entity must match all of given criteria. - * @return The required criteria. - */ - public Map getRequiredCriteria() { - return requiredCriteria; - } - - /** - * Returns the optional criteria. Map key represents property path and map value represents criteria. Each entity must match at least one of given criteria. - * @return The optional criteria. - */ - public Map getOptionalCriteria() { - return optionalCriteria; - } - - - // Object overrides ----------------------------------------------------------------------------------------------- - - @Override - public boolean equals(Object object) { - if (!(object instanceof Page)) { - return false; - } - - if (object == this) { - return true; - } - - Page other = (Page) object; - - return Objects.equals(offset, other.offset) - && Objects.equals(limit, other.limit) - && Objects.equals(last, other.last) - && Objects.equals(reversed, other.reversed) - && Objects.equals(ordering, other.ordering) - && Objects.equals(requiredCriteria, other.requiredCriteria) - && Objects.equals(optionalCriteria, other.optionalCriteria); - } - - @Override - public int hashCode() { - return Objects.hash(Page.class, offset, limit, last, reversed, ordering, requiredCriteria, optionalCriteria); - } - - @Override - public String toString() { - return new StringBuilder("Page[") - .append(offset).append(",") - .append(limit).append(",") - .append(last).append(",") - .append(reversed).append(",") - .append(ordering).append(",") - .append(new TreeMap<>(requiredCriteria)).append(",") - .append(new TreeMap<>(optionalCriteria)).append("]").toString(); - } - - - // Builder -------------------------------------------------------------------------------------------------------- - - /** - * Returns a clone of the current page which returns all results matching the current ordering, required criteria and optional criteria. - * @return A clone of the current page which returns all results matching the current ordering, required criteria and optional criteria. - */ - public Page all() { - return new Page(null, null, new LinkedHashMap<>(ordering), requiredCriteria, optionalCriteria); - } - - /** - * Use this if you want to build a new page. - * @return A new page builder. - */ - public static Builder with() { - return new Builder(); - } - - /** - * Use this if you want a page of given offset and limit. - * @param offset Zero-based offset of the page. May not be negative. Defaults to 0. - * @param limit Maximum amount of records to be matched. May not be less than 1. Defaults to {@link Integer#MAX_VALUE}. - * @return A new page of given offset and limit. - */ - public static Page of(int offset, int limit) { - return with().range(offset, limit).build(); - } - - /** - * The page builder. Use {@link Page#with()} to get started. - * @author Bauke Scholtz - */ - public static class Builder { - - private Integer offset; - private Integer limit; - private LinkedHashMap ordering = new LinkedHashMap<>(2); - private Map requiredCriteria; - private Map optionalCriteria; - - /** - * Set the range. - * @param offset Zero-based offset of the page. May not be negative. Defaults to 0. - * @param limit Maximum amount of records to be matched. May not be less than 1. Defaults to {@link Integer#MAX_VALUE}. - * @throws IllegalStateException When another offset and limit is already set in this builder. - * @return This builder. - */ - public Builder range(int offset, int limit) { - if (this.offset != null) { - throw new IllegalStateException("Offset and limit are already set"); - } - - this.offset = offset; - this.limit = limit; - return this; - } - - /** - * Set the ordering. This can be invoked multiple times and will be remembered in same order. The default ordering is {"id",false}. - * @param field The field. - * @param ascending Whether to sort ascending. - * @return This builder. - */ - public Builder orderBy(String field, boolean ascending) { - ordering.put(field, ascending); - return this; - } - - /** - * Set the required criteria. Map key represents property path and map value represents criteria. Each entity must match all of given criteria. - * @param requiredCriteria Required criteria. - * @return This builder. - * @throws IllegalStateException When another required criteria is already set in this builder. - * @see Criteria - */ - public Builder allMatch(Map requiredCriteria) { - if (this.requiredCriteria != null) { - throw new IllegalStateException("Required criteria is already set"); - } - - this.requiredCriteria = requiredCriteria; - return this; - } - - /** - * Set the optional criteria. Map key represents property path and map value represents criteria. Each entity must match at least one of given criteria. - * @param optionalCriteria Optional criteria. - * @return This builder. - * @throws IllegalStateException When another optional criteria is already set in this builder. - * @see Criteria - */ - public Builder anyMatch(Map optionalCriteria) { - if (this.optionalCriteria != null) { - throw new IllegalStateException("Optional criteria is already set"); - } - - this.optionalCriteria = optionalCriteria; - return this; - } - - /** - * Build the page. - * @return The built page. - */ - public Page build() { - return new Page(offset, limit, ordering, requiredCriteria, optionalCriteria); - } - - } + // Constants ------------------------------------------------------------------------------------------------------ + + public final static Page ALL = Page.of(0, MAX_VALUE); + public final static Page ONE = Page.of(0, 1); + + + // Properties ----------------------------------------------------------------------------------------------------- + + private final int offset; + private final int limit; + private final Identifiable last; + private final boolean reversed; + private final Map ordering; + private final Map requiredCriteria; + private final Map optionalCriteria; + + + // Constructors --------------------------------------------------------------------------------------------------- + + /** + * Creates a new Page. You can for convenience also use {@link Page#of(int, int)} or the {@link Page#with()} builder. + * @param offset Zero-based offset of the page. May not be negative. Defaults to 0. + * @param limit Maximum amount of records to be matched. May not be less than 1. Defaults to {@link Integer#MAX_VALUE}. + * @param ordering Ordering of results. Map key represents property path and map value represents whether to sort ascending. Defaults to {"id",false}. + * @param requiredCriteria Required criteria. Map key represents property path and map value represents criteria. Each entity must match all of given criteria. + * @param optionalCriteria Optional criteria. Map key represents property path and map value represents criteria. Each entity must match at least one of given criteria. + */ + public Page(Integer offset, Integer limit, LinkedHashMap ordering, Map requiredCriteria, Map optionalCriteria) { + this(offset, limit, null, null, ordering, requiredCriteria, optionalCriteria); + } + + /** + * Creates a new Page whereby value based paging will be performed instead of offset based paging when applicable. + * Value based paging is not applicable when the result type is a DTO, or when the ordering contains an aggregated field. + * @param offset Zero-based offset of the page. May not be negative. Defaults to 0. + * @param limit Maximum amount of records to be matched. May not be less than 1. Defaults to {@link Integer#MAX_VALUE}. + * @param last Last entity of the previous page. When not null, then value based paging will be performed instead of offset based paging when applicable. + * @param reversed Whether value based paging is reversed. This is ignored when last entity is null. Defaults to false. + * @param ordering Ordering of results. Map key represents property path and map value represents whether to sort ascending. Defaults to {"id",false}. + * @param requiredCriteria Required criteria. Map key represents property path and map value represents criteria. Each entity must match all of given criteria. + * @param optionalCriteria Optional criteria. Map key represents property path and map value represents criteria. Each entity must match at least one of given criteria. + */ + public Page(Integer offset, Integer limit, Identifiable last, Boolean reversed, LinkedHashMap ordering, Map requiredCriteria, Map optionalCriteria) { + this.offset = validateIntegerArgument("offset", offset, 0, 0); + this.limit = validateIntegerArgument("limit", limit, 1, MAX_VALUE); + this.last = last; + this.reversed = (last != null) && (reversed == TRUE); + this.ordering = !isEmpty(ordering) ? unmodifiableMap(ordering) : singletonMap(ID, false); + this.requiredCriteria = requiredCriteria != null ? unmodifiableMap(requiredCriteria) : emptyMap(); + this.optionalCriteria = optionalCriteria != null ? unmodifiableMap(optionalCriteria) : emptyMap(); + } + + private static int validateIntegerArgument(String argumentName, Integer argumentValue, int minValue, int defaultValue) { + if (argumentValue == null) { + return defaultValue; + } + + if (argumentValue < minValue) { + throw new IllegalArgumentException("Argument '" + argumentName + "' may not be less than " + minValue); + } + + return argumentValue; + } + + + // Getters -------------------------------------------------------------------------------------------------------- + + /** + * Returns the offset. Defaults to 0. + * @return The offset. + */ + public int getOffset() { + return offset; + } + + /** + * Returns the limit. Defaults to {@link Integer#MAX_VALUE}. + * @return The limit. + */ + public int getLimit() { + return limit; + } + + /** + * Returns the last entity of the previous page, if any. + * If not null, then value based paging will be performed instead of offset based paging when applicable. + * @return The last entity of the previous page, if any. + */ + public Identifiable getLast() { + return last; + } + + /** + * Returns whether the value based paging is reversed. + * This is only used when {@link #getLast()} is not null. + * @return Whether the value based paging is reversed. + */ + public boolean isReversed() { + return reversed; + } + + /** + * Returns the ordering. Map key represents property path and map value represents whether to sort ascending. Defaults to {"id",false}. + * @return The ordering. + */ + public Map getOrdering() { + return ordering; + } + + /** + * Returns the required criteria. Map key represents property path and map value represents criteria. Each entity must match all of given criteria. + * @return The required criteria. + */ + public Map getRequiredCriteria() { + return requiredCriteria; + } + + /** + * Returns the optional criteria. Map key represents property path and map value represents criteria. Each entity must match at least one of given criteria. + * @return The optional criteria. + */ + public Map getOptionalCriteria() { + return optionalCriteria; + } + + + // Object overrides ----------------------------------------------------------------------------------------------- + + @Override + public boolean equals(Object object) { + if (!(object instanceof Page)) { + return false; + } + + if (object == this) { + return true; + } + + Page other = (Page) object; + + return Objects.equals(offset, other.offset) + && Objects.equals(limit, other.limit) + && Objects.equals(last, other.last) + && Objects.equals(reversed, other.reversed) + && Objects.equals(ordering, other.ordering) + && Objects.equals(requiredCriteria, other.requiredCriteria) + && Objects.equals(optionalCriteria, other.optionalCriteria); + } + + @Override + public int hashCode() { + return Objects.hash(Page.class, offset, limit, last, reversed, ordering, requiredCriteria, optionalCriteria); + } + + @Override + public String toString() { + return new StringBuilder("Page[") + .append(offset).append(",") + .append(limit).append(",") + .append(last).append(",") + .append(reversed).append(",") + .append(ordering).append(",") + .append(new TreeMap<>(requiredCriteria)).append(",") + .append(new TreeMap<>(optionalCriteria)).append("]").toString(); + } + + + // Builder -------------------------------------------------------------------------------------------------------- + + /** + * Returns a clone of the current page which returns all results matching the current ordering, required criteria and optional criteria. + * @return A clone of the current page which returns all results matching the current ordering, required criteria and optional criteria. + */ + public Page all() { + return new Page(null, null, new LinkedHashMap<>(ordering), requiredCriteria, optionalCriteria); + } + + /** + * Use this if you want to build a new page. + * @return A new page builder. + */ + public static Builder with() { + return new Builder(); + } + + /** + * Use this if you want a page of given offset and limit. + * @param offset Zero-based offset of the page. May not be negative. Defaults to 0. + * @param limit Maximum amount of records to be matched. May not be less than 1. Defaults to {@link Integer#MAX_VALUE}. + * @return A new page of given offset and limit. + */ + public static Page of(int offset, int limit) { + return with().range(offset, limit).build(); + } + + /** + * The page builder. Use {@link Page#with()} to get started. + * @author Bauke Scholtz + */ + public static class Builder { + + private Integer offset; + private Integer limit; + private LinkedHashMap ordering = new LinkedHashMap<>(2); + private Map requiredCriteria; + private Map optionalCriteria; + + /** + * Set the range. + * @param offset Zero-based offset of the page. May not be negative. Defaults to 0. + * @param limit Maximum amount of records to be matched. May not be less than 1. Defaults to {@link Integer#MAX_VALUE}. + * @throws IllegalStateException When another offset and limit is already set in this builder. + * @return This builder. + */ + public Builder range(int offset, int limit) { + if (this.offset != null) { + throw new IllegalStateException("Offset and limit are already set"); + } + + this.offset = offset; + this.limit = limit; + return this; + } + + /** + * Set the ordering. This can be invoked multiple times and will be remembered in same order. The default ordering is {"id",false}. + * @param field The field. + * @param ascending Whether to sort ascending. + * @return This builder. + */ + public Builder orderBy(String field, boolean ascending) { + ordering.put(field, ascending); + return this; + } + + /** + * Set the required criteria. Map key represents property path and map value represents criteria. Each entity must match all of given criteria. + * @param requiredCriteria Required criteria. + * @return This builder. + * @throws IllegalStateException When another required criteria is already set in this builder. + * @see Criteria + */ + public Builder allMatch(Map requiredCriteria) { + if (this.requiredCriteria != null) { + throw new IllegalStateException("Required criteria is already set"); + } + + this.requiredCriteria = requiredCriteria; + return this; + } + + /** + * Set the optional criteria. Map key represents property path and map value represents criteria. Each entity must match at least one of given criteria. + * @param optionalCriteria Optional criteria. + * @return This builder. + * @throws IllegalStateException When another optional criteria is already set in this builder. + * @see Criteria + */ + public Builder anyMatch(Map optionalCriteria) { + if (this.optionalCriteria != null) { + throw new IllegalStateException("Optional criteria is already set"); + } + + this.optionalCriteria = optionalCriteria; + return this; + } + + /** + * Build the page. + * @return The built page. + */ + public Page build() { + return new Page(offset, limit, ordering, requiredCriteria, optionalCriteria); + } + + } } \ No newline at end of file diff --git a/src/main/java/org/omnifaces/persistence/service/Alias.java b/src/main/java/org/omnifaces/persistence/service/Alias.java index 09195b7..9d6bed6 100644 --- a/src/main/java/org/omnifaces/persistence/service/Alias.java +++ b/src/main/java/org/omnifaces/persistence/service/Alias.java @@ -26,56 +26,56 @@ */ class Alias { - private static final String AS = "as_"; - private static final String WHERE = "where_"; - private static final String HAVING = "having_"; - private static final String IN = "_in"; - - private String value; - - private Alias(String alias) { - this.value = alias; - } - - public static Selection as(Entry> mappingEntry) { - Selection selection = mappingEntry.getValue(); - return selection.getAlias() != null ? selection : selection.alias(AS + mappingEntry.getKey().replace('.', '$')); - } - - public static Alias create(Provider provider, Expression expression, String field) { - return new Alias((provider.isAggregation(expression) ? HAVING : WHERE) + field.replace('.', '$')); - } - - public void in(int count) { - value += "_" + count + IN; - } - - public void set(Predicate predicate) { - predicate.alias(value); - } - - public static boolean isWhere(Predicate predicate) { - return predicate.getAlias().startsWith(WHERE); - } - - public static boolean isIn(Predicate predicate) { - return predicate.getAlias().endsWith(IN); - } - - public static boolean isHaving(Predicate predicate) { - return predicate.getAlias().startsWith(HAVING); - } - - public static Entry getFieldAndCount(Predicate inPredicate) { - String alias = inPredicate.getAlias(); - String fieldAndCount = alias.substring(alias.indexOf('_') + 1, alias.lastIndexOf('_')); - String field = fieldAndCount.substring(0, fieldAndCount.lastIndexOf('_')).replace('$', '.'); - long count = Long.valueOf(fieldAndCount.substring(field.length() + 1)); - return new SimpleEntry<>(field, count); - } - - public static void setHaving(Predicate inPredicate, Predicate countPredicate) { - countPredicate.alias(HAVING + inPredicate.getAlias().substring(inPredicate.getAlias().indexOf('_') + 1)); - } + private static final String AS = "as_"; + private static final String WHERE = "where_"; + private static final String HAVING = "having_"; + private static final String IN = "_in"; + + private String value; + + private Alias(String alias) { + this.value = alias; + } + + public static Selection as(Entry> mappingEntry) { + Selection selection = mappingEntry.getValue(); + return selection.getAlias() != null ? selection : selection.alias(AS + mappingEntry.getKey().replace('.', '$')); + } + + public static Alias create(Provider provider, Expression expression, String field) { + return new Alias((provider.isAggregation(expression) ? HAVING : WHERE) + field.replace('.', '$')); + } + + public void in(int count) { + value += "_" + count + IN; + } + + public void set(Predicate predicate) { + predicate.alias(value); + } + + public static boolean isWhere(Predicate predicate) { + return predicate.getAlias().startsWith(WHERE); + } + + public static boolean isIn(Predicate predicate) { + return predicate.getAlias().endsWith(IN); + } + + public static boolean isHaving(Predicate predicate) { + return predicate.getAlias().startsWith(HAVING); + } + + public static Entry getFieldAndCount(Predicate inPredicate) { + String alias = inPredicate.getAlias(); + String fieldAndCount = alias.substring(alias.indexOf('_') + 1, alias.lastIndexOf('_')); + String field = fieldAndCount.substring(0, fieldAndCount.lastIndexOf('_')).replace('$', '.'); + long count = Long.valueOf(fieldAndCount.substring(field.length() + 1)); + return new SimpleEntry<>(field, count); + } + + public static void setHaving(Predicate inPredicate, Predicate countPredicate) { + countPredicate.alias(HAVING + inPredicate.getAlias().substring(inPredicate.getAlias().indexOf('_') + 1)); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/BaseEntityService.java b/src/main/java/org/omnifaces/persistence/service/BaseEntityService.java index 5bb3ed3..c60287c 100644 --- a/src/main/java/org/omnifaces/persistence/service/BaseEntityService.java +++ b/src/main/java/org/omnifaces/persistence/service/BaseEntityService.java @@ -183,2021 +183,2021 @@ */ 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 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 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> 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> 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> 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. - *
-	 * public abstract class YourBaseEntityService<E extends BaseEntity<Long>> extends BaseEntityService<Long, E> {
-	 *
-	 *     @PersistenceContext(unitName = "yourPersistenceUnitName")
-	 *     private EntityManager entityManager;
-	 *
-	 *     @Override
-	 *     public EntityManager getEntityManager() {
-	 *         return entityManager;
-	 *     }
-	 *
-	 * }
-	 * 
- * - * @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: - *

-	 * @Stateless
-	 * public class YourEntityService extends BaseEntityService<YourEntity> {
-	 *
-	 *     public List<YourEntity> getFooByType(Type type) {
-	 *         return list((criteriaBuilder, query, root) -> {
-	 *             query.where(criteriaBuilder.equal(root.get("type"), type));
-	 *         }, noop());
-	 *     }
-	 *
-	 * }
-	 * 
- * @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);
-	 * });
-	 * 
- *

- * Short jpql is also supported: - *

-	 * Optional<Foo> foo = find("WHERE bar = :bar AND 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> parameters) { - return getOptionalSingleResult(list(jpql, parameters)); - } - - /** - * Find entity by {@link CriteriaQueryBuilder} and mapped parameters, if any. - *

- * Usage example: - *

-	 * Optional<Foo> foo = find(
-	 * 		(criteriaBuilder, query, root) -> {
-	 * 			query.where(criteriaBuilder.equal(root.get("type"), criteriaBuilder.parameter(Type.class, "foo"));
-	 * 		},
-	 * 		params -> {
-	 *     		params.put("foo", Type.FOO);
-	 * 		}
-	 * );
-	 * 
- * @param queryBuilder This creates the JPA criteria query. - * @param parameters To put the mapped query parameters in. - * @return Found entity matching {@link CriteriaQueryBuilder} and mapped parameters, if any. - * @throws NonUniqueResultException When more than one entity is found matching given query and mapped parameters. - */ - protected Optional find(CriteriaQueryBuilder queryBuilder, Consumer> parameters) { - return getOptionalSingleResult(list(queryBuilder, parameters)); - } - - private Optional getOptionalSingleResult(List results) { - if (results.isEmpty()) { - return Optional.empty(); - } - else if (results.size() == 1) { - return Optional.of(results.get(0)); - } - else { - throw new NonUniqueResultException(); - } - } - - /** - * Find first entity by the given query and positional parameters, if any. - * The difference with {@link #find(String, Object...)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches. - *

- * Usage example: - *

-	 * Optional<Foo> foo = findFirst("SELECT f FROM Foo f WHERE f.bar = ?1 AND f.baz = ?2", bar, baz);
-	 * 
- *

- * Short jpql is also supported: - *

-	 * Optional<Foo> foo = findFirst("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. - */ - protected Optional findFirst(String jpql, Object... parameters) { - return getOptionalFirstResult(createQuery(select(jpql), parameters)); - } - - /** - * Find first entity by the given query and mapped parameters, if any. - * The difference with {@link #find(String, Consumer)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches. - *

- * Usage example: - *

-	 * Optional<Foo> foo = findFirst("SELECT f FROM Foo f WHERE f.bar = :bar AND f.baz = :baz", params -> {
-	 *     params.put("bar", bar);
-	 *     params.put("baz", baz);
-	 * });
-	 * 
- *

- * Short jpql is also supported: - *

-	 * Optional<Foo> foo = findFirst("WHERE bar = :bar AND 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. - */ - protected Optional findFirst(String jpql, Consumer> parameters) { - return getOptionalFirstResult(createQuery(select(jpql), parameters)); - } - - /** - * Find first entity by {@link CriteriaQueryBuilder} and mapped parameters, if any. - * The difference with {@link #find(CriteriaQueryBuilder, Consumer)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches. - *

- * Usage example: - *

-	 * Optional<Foo> foo = findFirst(
-	 * 		(criteriaBuilder, query, root) -> {
-	 * 			query.where(criteriaBuilder.equal(root.get("type"), criteriaBuilder.parameter(Type.class, "foo"));
-	 * 		},
-	 * 		params -> {
-	 *     		params.put("foo", Type.FOO);
-	 * 		}
-	 * );
-	 * 
- * @param queryBuilder This creates the JPA criteria query. - * @param parameters To put the mapped query parameters in. - * @return Found entity matching {@link CriteriaQueryBuilder} and mapped parameters, if any. - */ - protected Optional findFirst(CriteriaQueryBuilder queryBuilder, Consumer> parameters) { - return getOptionalFirstResult(createQuery(queryBuilder, parameters)); - } - - /** - * Find entity by the given ID. This does not include soft deleted one. - * @param id Entity ID to find entity for. - * @return Found entity, if any. - */ - public Optional findById(I id) { - return Optional.ofNullable(getById(id, false)); - } - - /** - * Find entity by the given ID and set whether it may return a soft deleted one. - * @param id Entity ID to find entity for. - * @param includeSoftDeleted Whether to include soft deleted ones in the search. - * @return Found entity, if any. - */ - protected Optional findById(I id, boolean includeSoftDeleted) { - return Optional.ofNullable(getById(id, includeSoftDeleted)); - } - - /** - * Find soft deleted entity by the given ID. - * @param id Entity ID to find soft deleted entity for. - * @return Found soft deleted entity, if any. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - */ - public Optional findSoftDeletedById(I id) { - return Optional.ofNullable(getSoftDeletedById(id)); - } - - /** - * Get entity by the given ID. This does not include soft deleted one. - * @param id Entity ID to get entity by. - * @return Found entity, or null if there is none. - */ - public E getById(I id) { - return getById(id, false); - } - - /** - * Get entity by the given ID and set whether it may return a soft deleted one. - * @param id Entity ID to get entity by. - * @param includeSoftDeleted Whether to include soft deleted ones in the search. - * @return Found entity, or null if there is none. - */ - protected E getById(I id, boolean includeSoftDeleted) { - var entity = getEntityManager().find(entityType, id); - - if (entity != null && !includeSoftDeleted && softDeleteData.isSoftDeleted(entity)) { - return null; - } - - return entity; - } - - /** - * Get entity by the given ID and entity graph name. - * @param id Entity ID to get entity by. - * @param entityGraphName Entity graph name. - * @return Found entity, or null if there is none. - */ - public E getByIdWithLoadGraph(I id, String entityGraphName) { - var entityGraph = entityManager.getEntityGraph(entityGraphName); - var properties = new HashMap(); - properties.put(QUERY_HINT_LOAD_GRAPH, entityGraph); - properties.put(QUERY_HINT_CACHE_RETRIEVE_MODE, BYPASS); - - return getEntityManager().find(entityType, id, properties); - } - - /** - * Get soft deleted entity by the given ID. - * @param id Entity ID to get soft deleted entity by. - * @return Found soft deleted entity, or null if there is none. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - */ - public E getSoftDeletedById(I id) { - softDeleteData.checkSoftDeletable(); - var entity = getEntityManager().find(entityType, id); - - if (entity != null && !softDeleteData.isSoftDeleted(entity)) { - return null; - } - - return entity; - } - - /** - * Get entities by the given IDs. The default ordering is by ID, descending. This does not include soft deleted ones. - * @param ids Entity IDs to get entities by. - * @return Found entities, or an empty set if there is none. - */ - public List getByIds(Iterable ids) { - return getByIds(ids, false); - } - - /** - * Get entities by the given IDs and set whether it may include soft deleted ones. The default ordering is by ID, descending. - * @param ids Entity IDs to get entities by. - * @param includeSoftDeleted Whether to include soft deleted ones in the search. - * @return Found entities, optionally including soft deleted ones, or an empty set if there is none. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - */ - protected List getByIds(Iterable ids, boolean includeSoftDeleted) { - if (!ids.iterator().hasNext()) { - return emptyList(); - } - - var whereClause = softDeleteData.getWhereClause(includeSoftDeleted); - return list(select("") - + whereClause + (whereClause.isEmpty() ? " WHERE" : " AND") + " e.id IN (:ids)" - + " ORDER BY e.id DESC", p -> p.put("ids", ids)); - } - - /** - * Check whether given entity exists. - * This method supports proxied entities. - * @param entity Entity to check. - * @return Whether entity with given entity exists. - */ - protected boolean exists(E entity) { - var id = getProvider().getIdentifier(entity); - return id != null && createLongQuery("SELECT COUNT(e) FROM " + entityType.getSimpleName() + " e WHERE e.id = :id") - .setParameter("id", id) - .getSingleResult().intValue() > 0; - } - - /** - * List all entities. The default ordering is by ID, descending. This does not include soft deleted entities. - * @return List of all entities. - */ - public List list() { - return list(false); - } - - /** - * List all entities and set whether it may include soft deleted ones. The default ordering is by ID, descending. - * @param includeSoftDeleted Whether to include soft deleted ones in the search. - * @return List of all entities, optionally including soft deleted ones. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - */ - protected List list(boolean includeSoftDeleted) { - return list(select("") - + softDeleteData.getWhereClause(includeSoftDeleted) - + " ORDER BY e.id DESC"); - } - - /** - * List all entities that have been soft deleted. The default ordering is by ID, descending. - * @return List of all soft deleted entities. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - */ - public List listSoftDeleted() { - softDeleteData.checkSoftDeletable(); - return list(select("") - + softDeleteData.getWhereClause(true) - + " ORDER BY e.id DESC"); - } - - /** - * List entities matching the given query and positional parameters, if any. - *

- * Usage example: - *

-	 * List<Foo> foos = list("SELECT f FROM Foo f WHERE f.bar = ?1 AND f.baz = ?2", bar, baz);
-	 * 
- *

- * Short jpql is also supported: - *

-	 * List<Foo> foos = list("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 List of entities matching the given query and positional parameters, if any. - */ - protected List list(String jpql, Object... parameters) { - return createQuery(select(jpql), parameters).getResultList(); - } - - /** - * List entities matching the given query and mapped parameters, if any. - *

- * Usage example: - *

-	 * List<Foo> foos = list("SELECT f FROM Foo f WHERE f.bar = :bar AND f.baz = :baz", params -> {
-	 *     params.put("bar", bar);
-	 *     params.put("baz", baz);
-	 * });
-	 * 
- *

- * Short jpql is also supported: - *

-	 * List<Foo> foos = list("WHERE bar = :bar AND 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 List of entities matching the given query and mapped parameters, if any. - */ - protected List list(String jpql, Consumer> parameters) { - return createQuery(select(jpql), parameters).getResultList(); - } - - /** - * List entities matching the {@link CriteriaQueryBuilder} and mapped parameters, if any. - *

- * Usage example: - *

-	 * List<Foo> foo = list(
-	 * 		(criteriaBuilder, query, root) -> {
-	 * 			query.where(criteriaBuilder.equal(root.get("type"), criteriaBuilder.parameter(Type.class, "foo"));
-	 * 		},
-	 * 		params -> {
-	 *     		params.put("foo", Type.FOO);
-	 * 		}
-	 * );
-	 * 
- * @param queryBuilder This creates the JPA criteria query. - * @param parameters To put the mapped query parameters in. - * @return List of entities matching the {@link CriteriaQueryBuilder} and mapped parameters, if any. - */ - protected List list(CriteriaQueryBuilder queryBuilder, Consumer> parameters) { - return createQuery(queryBuilder, parameters).getResultList(); - } - - private String select(String jpql) { - if (!startsWithOneOf(jpql.trim().toLowerCase(), "select", "from")) { - return "SELECT e FROM " + entityType.getSimpleName() + " e " + jpql; - } - else { - return jpql; - } - } - - private String update(String jpql) { - if (!jpql.trim().toLowerCase().startsWith("update")) { - return "UPDATE " + entityType.getSimpleName() + " e " + jpql; - } - else { - return jpql; - } - } - - private TypedQuery createQuery(String jpql, Object... parameters) { - var query = getEntityManager().createQuery(jpql, entityType); - setPositionalParameters(query, parameters); - return query; - } - - private TypedQuery createQuery(String jpql, Consumer> parameters) { - var query = getEntityManager().createQuery(jpql, entityType); - setSuppliedParameters(query, parameters); - return query; - } - - private TypedQuery createQuery(CriteriaQueryBuilder queryBuilder, Consumer> parameters) { - var criteriaBuilder = getEntityManager().getCriteriaBuilder(); - var criteriaQuery = criteriaBuilder.createQuery(entityType); - var root = buildRoot(criteriaQuery); - - queryBuilder.build(criteriaBuilder, criteriaQuery, root); - - var query = getEntityManager().createQuery(criteriaQuery); - - if (root instanceof EclipseLinkRoot eclipseLinkRoot) { - eclipseLinkRoot.runPostponedFetches(query); - } - - setSuppliedParameters(query, parameters); - return query; - } - - - // Insert actions ------------------------------------------------------------------------------------------------- - - /** - * Persist given entity and immediately perform a flush. - * Any bean validation constraint violation will be logged separately. - * @param entity Entity to persist. - * @return Entity ID. - * @throws IllegalEntityStateException When entity is already persisted or its ID is not generated. - */ - public I persist(E entity) { - if (entity.getId() != null) { - if (generatedId || exists(entity)) { - throw new IllegalEntityStateException(entity, "Entity is already persisted. Use update() instead."); - } - } - else if (!generatedId) { - throw new IllegalEntityStateException(entity, "Entity has no generated ID. You need to manually set it."); - } - - try { - getEntityManager().persist(entity); - - } - catch (ConstraintViolationException e) { - logConstraintViolations(e.getConstraintViolations()); - throw e; - } - - // Entity is not guaranteed to have been given an ID before either the TX commits or flush is called. - getEntityManager().flush(); - - return entity.getId(); - } - - - // Update actions ------------------------------------------------------------------------------------------------- - - /** - * Update given entity. If jakarta.persistence.validation.mode property in persistence.xml is explicitly set - * to CALLBACK (and thus not to its default of AUTO), then any bean validation constraint violation will be - * logged separately. Due to technical limitations, this effectively means that bean validation is invoked twice. First in this method - * in order to be able to obtain the constraint violations and then once more while JTA is committing the transaction, but is executed - * beyond the scope of this method. - * @param entity Entity to update. - * @return Updated entity. - * @throws IllegalEntityStateException When entity is not persisted or its ID is not generated. - */ - public E update(E entity) { - if (entity.getId() == null) { - if (generatedId) { - throw new IllegalEntityStateException(entity, "Entity is not persisted. Use persist() instead."); - } - else { - throw new IllegalEntityStateException(entity, "Entity has no generated ID. You need to manually set it."); - } - } - - if (!exists(entity)) { - throw new IllegalEntityStateException(entity, "Entity is not persisted. Use persist() instead."); - } - - if (validator != null) { - // EntityManager#merge() doesn't directly throw ConstraintViolationException without performing flush, so we can't put it in a - // try-catch, and we can't even use an @Interceptor as it happens in JTA side not in EJB side. Hence, we're manually performing - // bean validation here so that we can capture them. - logConstraintViolations(validator.validate(entity)); - } - - return getEntityManager().merge(entity); - } - - /** - * Update given entity via {@link #update(BaseEntity)} and immediately perform a flush so that all changes in - * managed entities so far in the current transaction are persisted. This is particularly useful when you intend - * to process the given entity further in an asynchronous service method. - * @param entity Entity to update. - * @return Updated entity. - * @throws IllegalEntityStateException When entity is not persisted or its ID is not generated. - */ - protected E updateAndFlush(E entity) { - var updatedEntity = update(entity); - getEntityManager().flush(); - return updatedEntity; - } - - private static void logConstraintViolations(Set> constraintViolations) { - constraintViolations.forEach(violation -> { - var constraintName = violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(); - var beanName = violation.getRootBeanClass().getSimpleName(); - var propertyName = violation.getPropertyPath().toString(); - var violationMessage = violation.getMessage(); - Object beanInstance = violation.getRootBean(); - logger.severe(format(LOG_SEVERE_CONSTRAINT_VIOLATION, constraintName, beanName, propertyName, violationMessage, beanInstance)); - }); - } - - /** - * Update given entities. - * @param entities Entities to update. - * @return Updated entities. - * @throws IllegalEntityStateException When at least one entity has no ID. - */ - public List update(Iterable entities) { - return stream(entities).map(this::update).toList(); - } - - /** - * Update or delete all entities matching the given query and positional parameters, if any. - *

- * Usage example: - *

-	 * int affectedRows = update("UPDATE Foo f SET f.bar = ?1 WHERE f.baz = ?2", bar, baz);
-	 * 
- *

- * Short jpql is also supported: - *

-	 * int affectedRows = update("SET bar = ?1 WHERE baz = ?2", bar, baz);
-	 * 
- * @param jpql The Java Persistence Query Language statement. - * @param parameters The positional query parameters, if any. - * @return The number of entities updated or deleted. - * @see Query#executeUpdate() - */ - protected int update(String jpql, Object... parameters) { - return createQuery(update(jpql), parameters).executeUpdate(); - } - - /** - * Update or delete all entities matching the given query and mapped parameters, if any. - *

- * Usage example: - *

-	 * int affectedRows = update("UPDATE Foo f SET f.bar = :bar WHERE f.baz = :baz", params -> {
-	 *     params.put("bar", bar);
-	 *     params.put("baz", baz);
-	 * });
-	 * 
- *

- * Short jpql is also supported: - *

-	 * int affectedRows = update("SET bar = :bar WHERE 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, if any. - * @return The number of entities updated or deleted. - * @see Query#executeUpdate() - */ - protected int update(String jpql, Consumer> parameters) { - return createQuery(update(jpql), parameters).executeUpdate(); - } - - /** - * Save given entity. This will automatically determine based on the presence of generated entity ID, - * or existence of an entity in the data store whether to {@link #persist(BaseEntity)} or to {@link #update(BaseEntity)}. - * @param entity Entity to save. - * @return Saved entity. - */ - public E save(E entity) { - if (generatedId && entity.getId() == null || !generatedId && !exists(entity)) { - persist(entity); - return entity; - } - else { - return update(entity); - } - } - - /** - * Save given entity via {@link #save(BaseEntity)} and immediately perform a flush so that all changes in - * managed entities so far in the current transaction are persisted. This is particularly useful when you intend - * to process the given entity further in an asynchronous service method. - * @param entity Entity to save. - * @return Saved entity. - */ - protected E saveAndFlush(E entity) { - var savedEntity = save(entity); - getEntityManager().flush(); - return savedEntity; - } - - - // Delete actions ------------------------------------------------------------------------------------------------- - - /** - * Delete given entity. - * @param entity Entity to delete. - * @throws NonDeletableEntityException When entity has {@link NonDeletable} annotation set. - * @throws IllegalEntityStateException When entity has no ID. - * @throws EntityNotFoundException When entity has in meanwhile been deleted. - */ - public void delete(E entity) { - if (entity.getClass().isAnnotationPresent(NonDeletable.class)) { - throw new NonDeletableEntityException(entity); - } - - getEntityManager().remove(manage(entity)); - - if (getProvider() != ECLIPSELINK || !getEntityManager().contains(entity)) { - entity.setId(null); - } - } - - /** - * Soft delete given entity. - * @param entity Entity to soft delete. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - * @throws IllegalEntityStateException When entity has no ID. - * @throws EntityNotFoundException When entity has in meanwhile been hard deleted. - */ - public void softDelete(E entity) { - softDeleteData.checkSoftDeletable(); - softDeleteData.setSoftDeleted(manage(entity), true); - } - - /** - * Soft undelete given entity. - * @param entity Entity to soft undelete. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - * @throws IllegalEntityStateException When entity has no ID. - * @throws EntityNotFoundException When entity has in meanwhile been hard deleted. - */ - public void softUndelete(E entity) { - softDeleteData.checkSoftDeletable(); - softDeleteData.setSoftDeleted(manage(entity), false); - } - - /** - * Delete given entities. - * @param entities Entities to delete. - * @throws NonDeletableEntityException When at least one entity has {@link NonDeletable} annotation set. - * @throws IllegalEntityStateException When at least one entity has no ID. - * @throws EntityNotFoundException When at least one entity has in meanwhile been deleted. - */ - public void delete(Iterable entities) { - entities.forEach(this::delete); - } - - /** - * Soft delete given entities. - * @param entities Entities to soft delete. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - * @throws IllegalEntityStateException When at least one entity has no ID. - * @throws EntityNotFoundException When at least one entity has in meanwhile been hard deleted. - */ - public void softDelete(Iterable entities) { - entities.forEach(this::softDelete); - } - - /** - * Soft undelete given entities. - * @param entities Entities to soft undelete. - * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. - * @throws IllegalEntityStateException When at least one entity has no ID. - * @throws EntityNotFoundException When at least one entity has in meanwhile been hard deleted. - */ - public void softUndelete(Iterable entities) { - entities.forEach(this::softUndelete); - } - - - // Manage actions ------------------------------------------------------------------------------------------------- - - /** - * Make given entity managed. NOTE: This will discard any unmanaged changes in the given entity! - * This is particularly useful in case you intend to make sure that you have the most recent version at hands. - * This method also supports proxied entities as well as DTOs. - * @param entity Entity to manage. - * @return The managed entity. - * @throws NullPointerException When given entity is null. - * @throws IllegalEntityStateException When entity has no ID. - * @throws EntityNotFoundException When entity has in meanwhile been deleted. - */ - protected E manage(E entity) { - if (entity == null) { - throw new NullPointerException("Entity is null."); - } - - var id = getProvider().getIdentifier(entity); - - if (id == null) { - throw new IllegalEntityStateException(entity, "Entity has no ID."); - } - - if (entity.getClass().getAnnotation(Entity.class) != null && getEntityManager().contains(entity)) { - return entity; - } - - var managed = getEntityManager().find(getProvider().getEntityType(entity), id); - - if (managed == null) { - throw new EntityNotFoundException("Entity has in meanwhile been deleted."); - } - - return managed; - } - - /** - * Make any given entity managed if necessary. NOTE: This will discard any unmanaged changes in the given entity! - * This is particularly useful in case you intend to make sure that you have the most recent version at hands. - * This method also supports null entities as well as proxied entities as well as DTOs. - * @param The generic entity type. - * @param entity Entity to manage, may be null. - * @return The managed entity, or null when null was supplied. - * It leniently returns the very same argument if the entity has no ID or has been deleted in the meanwhile. - * @throws IllegalArgumentException When the given entity is actually not an instance of {@link BaseEntity}. - */ - @SuppressWarnings({ "hiding", "unchecked" }) - protected E manageIfNecessary(E entity) { - if (entity == null) { - return null; - } - - if (!(entity instanceof BaseEntity baseEntity)) { - throw new IllegalArgumentException(); - } - - var id = getProvider().getIdentifier(baseEntity); - - if (id == null || entity.getClass().getAnnotation(Entity.class) != null && getEntityManager().contains(entity)) { - return entity; - } - - return coalesce((E) getEntityManager().find(getProvider().getEntityType(baseEntity), id), entity); - } - - /** - * Reset given entity. This will discard any changes in given entity. The given entity must be unmanaged/detached. - * The actual intent of this method is to have the opportunity to completely reset the state of a given entity - * which might have been edited in the client, without changing the reference. This is generally useful when the - * entity is in turn held in some collection and you'd rather not manually remove and reinsert it in the collection. - * This method supports proxied entities. - * @param entity Entity to reset. - * @throws IllegalEntityStateException When entity is already managed, or has no ID. - * @throws EntityNotFoundException When entity has in meanwhile been deleted. - */ - public void reset(E entity) { - if (!getProvider().isProxy(entity) && getEntityManager().contains(entity)) { - throw new IllegalEntityStateException(entity, "Only unmanaged entities can be resetted."); - } - - var managed = manage(entity); - getMetamodel(entity).getAttributes().stream().map(Attribute::getJavaMember).filter(Field.class::isInstance).forEach(field -> map(field, managed, entity)); - // Note: EntityManager#refresh() is insuitable as it requires a managed entity and thus merge() could unintentionally persist changes before resetting. - } - - - // Count actions -------------------------------------------------------------------------------------------------- - - /** - * Returns count of all foreign key references to given entity. - * This is particularly useful in case you intend to check if the given entity is still referenced elsewhere in database. - * @param entity Entity to count all foreign key references for. - * @return Count of all foreign key references to given entity. - */ - protected long countForeignKeyReferencesTo(E entity) { - return countForeignKeyReferences(getEntityManager(), entityType, identifierType, manage(entity).getId()); - } - - - // Lazy fetching actions ------------------------------------------------------------------------------------------ - - /** - * Fetch lazy collections of given entity on given getters. If no getters are supplied, then it will fetch every - * single {@link PluralAttribute} not of type {@link CollectionType#MAP}. - * Note that the implementation does for simplicitly not check if those are actually lazy or eager. - *

- * Usage example: - *

-	 * Foo fooWithBarsAndBazs = fetchLazyCollections(getById(fooId), Foo::getBars, Foo::getBazs);
-	 * 
- * @param entity Entity instance to fetch lazy collections on. - * @param getters Getters of those lazy collections. - * @return The same entity, useful if you want to continue using it immediately. - */ - @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. - protected E fetchLazyCollections(E entity, Function>... getters) { - return fetchPluralAttributes(entity, type -> type != MAP, getters); - } - - /** - * Fetch lazy collections of given optional entity on given getters. If no getters are supplied, then it will fetch - * every single {@link PluralAttribute} not of type {@link CollectionType#MAP}. - * Note that the implementation does for simplicitly not check if those are actually lazy or eager. - *

- * Usage example: - *

-	 * Optional<Foo> fooWithBarsAndBazs = fetchLazyCollections(findById(fooId), Foo::getBars, Foo::getBazs);
-	 * 
- * @param entity Optional entity instance to fetch lazy collections on. - * @param getters Getters of those lazy collections. - * @return The same optional entity, useful if you want to continue using it immediately. - */ - @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. - protected Optional fetchLazyCollections(Optional entity, Function>... getters) { - return ofNullable(entity.isPresent() ? fetchLazyCollections(entity.get(), getters) : null); - } - - /** - * Fetch lazy maps of given entity on given getters. If no getters are supplied, then it will fetch every single - * {@link PluralAttribute} of type {@link CollectionType#MAP}. - * Note that the implementation does for simplicitly not check if those are actually lazy or eager. - *

- * Usage example: - *

-	 * Foo fooWithBarsAndBazs = fetchLazyCollections(getById(fooId), Foo::getBars, Foo::getBazs);
-	 * 
- * @param entity Entity instance to fetch lazy maps on. - * @param getters Getters of those lazy collections. - * @return The same entity, useful if you want to continue using it immediately. - */ - @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. - protected E fetchLazyMaps(E entity, Function>... getters) { - return fetchPluralAttributes(entity, type -> type == MAP, getters); - } - - /** - * Fetch lazy maps of given optional entity on given getters. If no getters are supplied, then it will fetch every - * single {@link PluralAttribute} of type {@link CollectionType#MAP}. - * Note that the implementation does for simplicitly not check if those are actually lazy or eager. - *

- * Usage example: - *

-	 * Optional<Foo> fooWithBarsAndBazs = fetchLazyCollections(findById(fooId), Foo::getBars, Foo::getBazs);
-	 * 
- * @param entity Optional entity instance to fetch lazy maps on. - * @param getters Getters of those lazy collections. - * @return The same optional entity, useful if you want to continue using it immediately. - */ - @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. - protected Optional fetchLazyMaps(Optional entity, Function>... getters) { - return ofNullable(entity.isPresent() ? fetchLazyMaps(entity.get(), getters) : null); - } - - /** - * Fetch all lazy blobs of given entity. - * Note that the implementation does for simplicitly not check if those are actually lazy or eager. - * @param entity Entity instance to fetch all blobs on. - * @return The same entity, useful if you want to continue using it immediately. - */ - protected E fetchLazyBlobs(E entity) { - return fetchSingularAttributes(entity, type -> type == byte[].class); - } - - /** - * Fetch all lazy blobs of given optional entity. - * Note that the implementation does for simplicitly not check if those are actually lazy or eager. - * @param entity Optional entity instance to fetch all blobs on. - * @return The same optional entity, useful if you want to continue using it immediately. - */ - protected Optional fetchLazyBlobs(Optional entity) { - return ofNullable(entity.isPresent() ? fetchLazyBlobs(entity.get()) : null); - } - - @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. - private E fetchPluralAttributes(E entity, java.util.function.Predicate ofType, Function... getters) { - if (isEmpty(getters)) { - for (var a : getMetamodel().getPluralAttributes()) { - if (ofType.test(a.getCollectionType())) { - ofNullable(invokeGetter(entity, a.getName())).ifPresent(c -> invokeMethod(c, "size")); - } - } - } - else { - stream(getters).forEach(getter -> ofNullable(getter.apply(entity)).ifPresent(c -> invokeMethod(c, "size"))); - } - - return entity; - } - - private E fetchSingularAttributes(E entity, java.util.function.Predicate> ofType) { - var managed = getById(entity.getId()); - - for (var a : getMetamodel().getSingularAttributes()) { - if (ofType.test(a.getJavaType())) { - var name = capitalize(a.getName()); - invokeSetter(entity, name, invokeGetter(managed, name)); - } - } - - return entity; - } - - - // Paging actions ------------------------------------------------------------------------------------------------- - - /** - * Functional interface to fine-grain a JPA criteria query for any of {@link #getPage(Page, boolean)} methods. - *

- * You do not need this interface directly. Just supply a lambda. Below is an usage example: - *

-	 * @Stateless
-	 * public class YourEntityService extends BaseEntityService<YourEntity> {
-	 *
-	 *     public void getPageOfFooType(Page page, boolean count) {
-	 *         return getPage(page, count, (criteriaBuilder, query, root) -> {
-	 *             query.where(criteriaBuilder.equal(root.get("type"), Type.FOO));
-	 *         });
-	 *     }
-	 *
-	 * }
-	 * 
- * @param The generic base entity type. - */ - @FunctionalInterface - protected interface QueryBuilder { - void build(CriteriaBuilder criteriaBuilder, AbstractQuery query, Root root); - } - - /** - * Functional interface to fine-grain a JPA criteria query for any of {@link #getPage(Page, boolean)} methods taking - * a specific result type, such as an entity subclass (DTO). You must return a {@link LinkedHashMap} with - * {@link Getter} as key and {@link Expression} as value. The mapping must be in exactly the same order as - * constructor arguments of your DTO. - *

- * You do not need this interface directly. Just supply a lambda. Below is an usage example: - *

-	 * public class YourEntityDTO extends YourEntity {
-	 *
-	 *     private BigDecimal totalPrice;
-	 *
-	 *     public YourEntityDTO(Long id, String name, BigDecimal totalPrice) {
-	 *         setId(id);
-	 *         setName(name);
-	 *         this.totalPrice = totalPrice;
-	 *     }
-	 *
-	 *     public BigDecimal getTotalPrice() {
-	 *         return totalPrice;
-	 *     }
-	 *
-	 * }
-	 * 
- *
-	 * @Stateless
-	 * public class YourEntityService extends BaseEntityService<YourEntity> {
-	 *
-	 *     public void getPageOfYourEntityDTO(Page page, boolean count) {
-	 *         return getPage(page, count, YourEntityDTO.class (criteriaBuilder, query, root) -> {
-	 *             Join<YourEntityDTO, YourChildEntity> child = root.join("child");
-	 *
-	 *             LinkedHashMap<Getter<YourEntityDTO>, Expression<?>> mapping = new LinkedHashMap<>();
-	 *             mapping.put(YourEntityDTO::getId, root.get("id"));
-	 *             mapping.put(YourEntityDTO::getName, root.get("name"));
-	 *             mapping.put(YourEntityDTO::getTotalPrice, builder.sum(child.get("price")));
-	 *
-	 *             return mapping;
-	 *         });
-	 *     }
-	 *
-	 * }
-	 * 
- * @param The generic base entity type or from a DTO subclass thereof. - */ - @FunctionalInterface - protected interface MappedQueryBuilder { - LinkedHashMap, Expression> build(CriteriaBuilder criteriaBuilder, AbstractQuery query, Root root); - } - - /** - * Here you can in your {@link BaseEntityService} subclass define the callback method which needs to be invoked before any of - * {@link #getPage(Page, boolean)} methods is called. For example, to set a vendor specific {@link EntityManager} hint. - * The default implementation returns a no-op callback. - * @return The callback method which is invoked before any of {@link #getPage(Page, boolean)} methods is called. - */ - protected Consumer beforePage() { - return entityManager -> noop(); - } - - /** - * Here you can in your {@link BaseEntityService} subclass define the callback method which needs to be invoked when any query involved in - * {@link #getPage(Page, boolean)} is about to be executed. For example, to set a vendor specific {@link Query} hint. - * The default implementation sets Hibernate, EclipseLink and JPA 2.0 cache-related hints. When cacheable argument is - * true, then it reads from cache where applicable, else it will read from DB and force a refresh of cache. Note that - * this is not supported by OpenJPA. - * @param The generic type of the entity or a DTO subclass thereof. - * @param resultType The result type which can be the entity type itself or a DTO subclass thereof. - * @param cacheable Whether the results should be cacheable. - * @return The callback method which is invoked when any query involved in {@link #getPage(Page, boolean)} is about - * to be executed. - */ - protected Consumer> onPage(Class resultType, boolean cacheable) { - return typedQuery -> { - if (getProvider() == HIBERNATE) { - typedQuery - .setHint(QUERY_HINT_HIBERNATE_CACHEABLE, cacheable); - } - else if (getProvider() == ECLIPSELINK) { - typedQuery - .setHint(QUERY_HINT_ECLIPSELINK_MAINTAIN_CACHE, cacheable) - .setHint(QUERY_HINT_ECLIPSELINK_REFRESH, !cacheable); - } - - if (getProvider() != OPENJPA) { - // OpenJPA doesn't support 2nd level cache. - typedQuery - .setHint(QUERY_HINT_CACHE_STORE_MODE, cacheable ? CacheStoreMode.USE : CacheStoreMode.REFRESH) - .setHint(QUERY_HINT_CACHE_RETRIEVE_MODE, cacheable ? CacheRetrieveMode.USE : CacheRetrieveMode.BYPASS); - } - }; - } - - /** - * Here you can in your {@link BaseEntityService} subclass define the callback method which needs to be invoked after any of - * {@link #getPage(Page, boolean)} methods is called. For example, to remove a vendor specific {@link EntityManager} hint. - * The default implementation returns a no-op callback. - * @return The callback method which is invoked after any of {@link #getPage(Page, boolean)} methods is called. - */ - protected Consumer afterPage() { - return entityManager -> noop(); - } - - /** - * Returns a partial result list based on given {@link Page}. This will by default cache the results. - *

- * Usage examples: - *

-	 * Page first10Records = Page.of(0, 10);
-	 * PartialResultList<Foo> foos = getPage(first10Records, true);
-	 * 
- *
-	 * Map<String, Object> criteria = new HashMap<>();
-	 * criteria.put("bar", bar); // Exact match.
-	 * criteria.put("baz", IgnoreCase.value(baz)); // Case insensitive match.
-	 * criteria.put("faz", Like.contains(faz)); // Case insensitive LIKE match.
-	 * criteria.put("kaz", Order.greaterThan(kaz)); // Greater than match.
-	 * Page first10RecordsMatchingCriteriaOrderedByBar = Page.with().allMatch(criteria).orderBy("bar", true).range(0, 10);
-	 * PartialResultList<Foo> foos = getPage(first10RecordsMatchingCriteriaOrderedByBar, true);
-	 * 
- * @param page The page to return a partial result list for. - * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be - * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. - * @return A partial result list based on given {@link Page}. - * @see Page - * @see Criteria - */ - public PartialResultList getPage(Page page, boolean count) { - // Implementation notice: we can't remove this getPage() method and rely on the other getPage() method with varargs below, - // because the one with varargs is incompatible as method reference for getPage(Page, boolean) in some Java versions. - // See https://github.com/omnifaces/omnipersistence/issues/11 - return getPage(page, count, true, entityType, (builder, query, root) -> noop()); - } - - /** - * Returns a partial result list based on given {@link Page} and fetch fields. This will by default cache the results. - *

- * Usage examples: - *

-	 * Page first10Records = Page.of(0, 10);
-	 * PartialResultList<Foo> foosWithBars = getPage(first10Records, true, "bar");
-	 * 
- *
-	 * Map<String, Object> criteria = new HashMap<>();
-	 * criteria.put("bar", bar); // Exact match.
-	 * criteria.put("baz", IgnoreCase.value(baz)); // Case insensitive match.
-	 * criteria.put("faz", Like.contains(faz)); // Case insensitive LIKE match.
-	 * criteria.put("kaz", Order.greaterThan(kaz)); // Greater than match.
-	 * Page first10RecordsMatchingCriteriaOrderedByBar = Page.with().allMatch(criteria).orderBy("bar", true).range(0, 10);
-	 * PartialResultList<Foo> foosWithBars = getPage(first10RecordsMatchingCriteriaOrderedByBar, true, "bar");
-	 * 
- * @param page The page to return a partial result list for. - * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be - * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. - * @param fetchFields Optionally, all (lazy loaded) fields to be explicitly fetched during the query. Each field - * can represent a JavaBean path, like as you would do in EL, such as parent.child.subchild. - * @return A partial result list based on given {@link Page}. - * @see Page - * @see Criteria - */ - protected PartialResultList getPage(Page page, boolean count, String... fetchFields) { - return getPage(page, count, true, fetchFields); - } - - /** - * Returns a partial result list based on given {@link Page} with the option whether to cache the results or not. - *

- * Usage example: see {@link #getPage(Page, boolean)} and {@link #getPage(Page, boolean, String...)}. - * @param page The page to return a partial result list for. - * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be - * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. - * @param cacheable Whether the results should be cacheable. - * @param fetchFields Optionally, all (lazy loaded) fields to be explicitly fetched during the query. Each field - * can represent a JavaBean path, like as you would do in EL, such as parent.child.subchild. - * @return A partial result list based on given {@link Page}. - * @see Page - * @see Criteria - */ - protected PartialResultList getPage(Page page, boolean count, boolean cacheable, String... fetchFields) { - return getPage(page, count, cacheable, entityType, (builder, query, root) -> { - for (var fetchField : fetchFields) { - FetchParent fetchParent = root; - - for (var attribute : fetchField.split("\\.")) { - fetchParent = fetchParent.fetch(attribute); - } - } - - return null; - }); - } - - /** - * Returns a partial result list based on given {@link Page} and {@link QueryBuilder}. This will by default cache - * the results. - *

- * Usage example: see {@link QueryBuilder}. - * @param page The page to return a partial result list for. - * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be - * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. - * @param queryBuilder This allows fine-graining the JPA criteria query. - * @return A partial result list based on given {@link Page} and {@link QueryBuilder}. - * @see Page - * @see Criteria - */ - protected PartialResultList getPage(Page page, boolean count, QueryBuilder queryBuilder) { - return getPage(page, count, true, queryBuilder); - } - - /** - * Returns a partial result list based on given {@link Page}, entity type and {@link QueryBuilder} with the option - * whether to cache the results or not. - *

- * Usage example: see {@link QueryBuilder}. - * @param page The page to return a partial result list for. - * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be - * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. - * @param cacheable Whether the results should be cacheable. - * @param queryBuilder This allows fine-graining the JPA criteria query. - * @return A partial result list based on given {@link Page} and {@link QueryBuilder}. - * @see Page - * @see Criteria - */ - @SuppressWarnings("unchecked") - protected PartialResultList getPage(Page page, boolean count, boolean cacheable, QueryBuilder queryBuilder) { - return getPage(page, count, cacheable, entityType, (builder, query, root) -> { - queryBuilder.build(builder, query, (Root) root); - return new LinkedHashMap<>(0); - }); - } - - /** - * Returns a partial result list based on given {@link Page}, result type and {@link MappedQueryBuilder}. This will - * by default cache the results. - *

- * Usage example: see {@link MappedQueryBuilder}. - * @param The generic type of the entity or a DTO subclass thereof. - * @param page The page to return a partial result list for. - * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be - * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. - * @param resultType The result type which can be the entity type itself or a DTO subclass thereof. - * @param mappedQueryBuilder This allows fine-graining the JPA criteria query and must return a mapping of - * getters-paths. - * @return A partial result list based on given {@link Page} and {@link MappedQueryBuilder}. - * @throws IllegalArgumentException When the result type does not equal entity type and mapping is empty. - * @see Page - * @see Criteria - */ - protected PartialResultList getPage(Page page, boolean count, Class resultType, MappedQueryBuilder mappedQueryBuilder) { - return getPage(page, count, true, resultType, mappedQueryBuilder); - } - - /** - * Returns a partial result list based on given {@link Page}, entity type and {@link QueryBuilder} with the option - * whether to cache the results or not. - *

- * Usage example: see {@link MappedQueryBuilder}. - * @param The generic type of the entity or a DTO subclass thereof. - * @param page The page to return a partial result list for. - * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be - * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. - * @param cacheable Whether the results should be cacheable. - * @param resultType The result type which can be the entity type itself or a DTO subclass thereof. - * @param queryBuilder This allows fine-graining the JPA criteria query and must return a mapping of - * getters-paths when result type does not equal entity type. - * @return A partial result list based on given {@link Page} and {@link MappedQueryBuilder}. - * @throws IllegalArgumentException When the result type does not equal entity type and mapping is empty. - * @see Page - * @see Criteria - */ - protected PartialResultList getPage(Page page, boolean count, boolean cacheable, Class resultType, MappedQueryBuilder queryBuilder) { - beforePage().accept(getEntityManager()); - - try { - logger.log(FINER, () -> format(LOG_FINER_GET_PAGE, page, count, cacheable, resultType)); - var pageBuilder = new PageBuilder<>(page, cacheable, resultType, queryBuilder); - var criteriaBuilder = getEntityManager().getCriteriaBuilder(); - TypedQuery entityQuery = buildEntityQuery(pageBuilder, criteriaBuilder); - var countQuery = count ? buildCountQuery(pageBuilder, criteriaBuilder) : null; - PartialResultList resultList = executeQuery(pageBuilder, entityQuery, countQuery); - logger.log(FINER, () -> format(LOG_FINER_QUERY_RESULT, resultList, resultList.getEstimatedTotalNumberOfResults())); - return resultList; - } - finally { - afterPage().accept(getEntityManager()); - } - } - - - // Query actions -------------------------------------------------------------------------------------------------- - - private TypedQuery buildEntityQuery(PageBuilder pageBuilder, CriteriaBuilder criteriaBuilder) { - var entityQuery = criteriaBuilder.createQuery(pageBuilder.getResultType()); - var entityQueryRoot = buildRoot(entityQuery); - var pathResolver = buildSelection(pageBuilder, entityQuery, entityQueryRoot, criteriaBuilder); - buildOrderBy(pageBuilder, entityQuery, criteriaBuilder, pathResolver); - return buildTypedQuery(pageBuilder, entityQuery, entityQueryRoot, buildRestrictions(pageBuilder, entityQuery, criteriaBuilder, pathResolver)); - } - - private TypedQuery buildCountQuery(PageBuilder pageBuilder, CriteriaBuilder criteriaBuilder) { - var countQuery = criteriaBuilder.createQuery(Long.class); - var countQueryRoot = countQuery.from(entityType); - countQuery.select(criteriaBuilder.count(countQueryRoot)); + 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 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> 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> 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> 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. + *

+     * public abstract class YourBaseEntityService<E extends BaseEntity<Long>> extends BaseEntityService<Long, E> {
+     *
+     *     @PersistenceContext(unitName = "yourPersistenceUnitName")
+     *     private EntityManager entityManager;
+     *
+     *     @Override
+     *     public EntityManager getEntityManager() {
+     *         return entityManager;
+     *     }
+     *
+     * }
+     * 
+ * + * @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: + *

+     * @Stateless
+     * public class YourEntityService extends BaseEntityService<YourEntity> {
+     *
+     *     public List<YourEntity> getFooByType(Type type) {
+     *         return list((criteriaBuilder, query, root) -> {
+     *             query.where(criteriaBuilder.equal(root.get("type"), type));
+     *         }, noop());
+     *     }
+     *
+     * }
+     * 
+ * @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);
+     * });
+     * 
+ *

+ * Short jpql is also supported: + *

+     * Optional<Foo> foo = find("WHERE bar = :bar AND 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> parameters) { + return getOptionalSingleResult(list(jpql, parameters)); + } + + /** + * Find entity by {@link CriteriaQueryBuilder} and mapped parameters, if any. + *

+ * Usage example: + *

+     * Optional<Foo> foo = find(
+     *         (criteriaBuilder, query, root) -> {
+     *             query.where(criteriaBuilder.equal(root.get("type"), criteriaBuilder.parameter(Type.class, "foo"));
+     *         },
+     *         params -> {
+     *             params.put("foo", Type.FOO);
+     *         }
+     * );
+     * 
+ * @param queryBuilder This creates the JPA criteria query. + * @param parameters To put the mapped query parameters in. + * @return Found entity matching {@link CriteriaQueryBuilder} and mapped parameters, if any. + * @throws NonUniqueResultException When more than one entity is found matching given query and mapped parameters. + */ + protected Optional find(CriteriaQueryBuilder queryBuilder, Consumer> parameters) { + return getOptionalSingleResult(list(queryBuilder, parameters)); + } + + private Optional getOptionalSingleResult(List results) { + if (results.isEmpty()) { + return Optional.empty(); + } + else if (results.size() == 1) { + return Optional.of(results.get(0)); + } + else { + throw new NonUniqueResultException(); + } + } + + /** + * Find first entity by the given query and positional parameters, if any. + * The difference with {@link #find(String, Object...)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches. + *

+ * Usage example: + *

+     * Optional<Foo> foo = findFirst("SELECT f FROM Foo f WHERE f.bar = ?1 AND f.baz = ?2", bar, baz);
+     * 
+ *

+ * Short jpql is also supported: + *

+     * Optional<Foo> foo = findFirst("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. + */ + protected Optional findFirst(String jpql, Object... parameters) { + return getOptionalFirstResult(createQuery(select(jpql), parameters)); + } + + /** + * Find first entity by the given query and mapped parameters, if any. + * The difference with {@link #find(String, Consumer)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches. + *

+ * Usage example: + *

+     * Optional<Foo> foo = findFirst("SELECT f FROM Foo f WHERE f.bar = :bar AND f.baz = :baz", params -> {
+     *     params.put("bar", bar);
+     *     params.put("baz", baz);
+     * });
+     * 
+ *

+ * Short jpql is also supported: + *

+     * Optional<Foo> foo = findFirst("WHERE bar = :bar AND 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. + */ + protected Optional findFirst(String jpql, Consumer> parameters) { + return getOptionalFirstResult(createQuery(select(jpql), parameters)); + } + + /** + * Find first entity by {@link CriteriaQueryBuilder} and mapped parameters, if any. + * The difference with {@link #find(CriteriaQueryBuilder, Consumer)} is that it doesn't throw {@link NonUniqueResultException} when there are multiple matches. + *

+ * Usage example: + *

+     * Optional<Foo> foo = findFirst(
+     *         (criteriaBuilder, query, root) -> {
+     *             query.where(criteriaBuilder.equal(root.get("type"), criteriaBuilder.parameter(Type.class, "foo"));
+     *         },
+     *         params -> {
+     *             params.put("foo", Type.FOO);
+     *         }
+     * );
+     * 
+ * @param queryBuilder This creates the JPA criteria query. + * @param parameters To put the mapped query parameters in. + * @return Found entity matching {@link CriteriaQueryBuilder} and mapped parameters, if any. + */ + protected Optional findFirst(CriteriaQueryBuilder queryBuilder, Consumer> parameters) { + return getOptionalFirstResult(createQuery(queryBuilder, parameters)); + } + + /** + * Find entity by the given ID. This does not include soft deleted one. + * @param id Entity ID to find entity for. + * @return Found entity, if any. + */ + public Optional findById(I id) { + return Optional.ofNullable(getById(id, false)); + } + + /** + * Find entity by the given ID and set whether it may return a soft deleted one. + * @param id Entity ID to find entity for. + * @param includeSoftDeleted Whether to include soft deleted ones in the search. + * @return Found entity, if any. + */ + protected Optional findById(I id, boolean includeSoftDeleted) { + return Optional.ofNullable(getById(id, includeSoftDeleted)); + } + + /** + * Find soft deleted entity by the given ID. + * @param id Entity ID to find soft deleted entity for. + * @return Found soft deleted entity, if any. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + */ + public Optional findSoftDeletedById(I id) { + return Optional.ofNullable(getSoftDeletedById(id)); + } + + /** + * Get entity by the given ID. This does not include soft deleted one. + * @param id Entity ID to get entity by. + * @return Found entity, or null if there is none. + */ + public E getById(I id) { + return getById(id, false); + } + + /** + * Get entity by the given ID and set whether it may return a soft deleted one. + * @param id Entity ID to get entity by. + * @param includeSoftDeleted Whether to include soft deleted ones in the search. + * @return Found entity, or null if there is none. + */ + protected E getById(I id, boolean includeSoftDeleted) { + var entity = getEntityManager().find(entityType, id); + + if (entity != null && !includeSoftDeleted && softDeleteData.isSoftDeleted(entity)) { + return null; + } + + return entity; + } + + /** + * Get entity by the given ID and entity graph name. + * @param id Entity ID to get entity by. + * @param entityGraphName Entity graph name. + * @return Found entity, or null if there is none. + */ + public E getByIdWithLoadGraph(I id, String entityGraphName) { + var entityGraph = entityManager.getEntityGraph(entityGraphName); + var properties = new HashMap(); + properties.put(QUERY_HINT_LOAD_GRAPH, entityGraph); + properties.put(QUERY_HINT_CACHE_RETRIEVE_MODE, BYPASS); + + return getEntityManager().find(entityType, id, properties); + } + + /** + * Get soft deleted entity by the given ID. + * @param id Entity ID to get soft deleted entity by. + * @return Found soft deleted entity, or null if there is none. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + */ + public E getSoftDeletedById(I id) { + softDeleteData.checkSoftDeletable(); + var entity = getEntityManager().find(entityType, id); + + if (entity != null && !softDeleteData.isSoftDeleted(entity)) { + return null; + } + + return entity; + } + + /** + * Get entities by the given IDs. The default ordering is by ID, descending. This does not include soft deleted ones. + * @param ids Entity IDs to get entities by. + * @return Found entities, or an empty set if there is none. + */ + public List getByIds(Iterable ids) { + return getByIds(ids, false); + } + + /** + * Get entities by the given IDs and set whether it may include soft deleted ones. The default ordering is by ID, descending. + * @param ids Entity IDs to get entities by. + * @param includeSoftDeleted Whether to include soft deleted ones in the search. + * @return Found entities, optionally including soft deleted ones, or an empty set if there is none. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + */ + protected List getByIds(Iterable ids, boolean includeSoftDeleted) { + if (!ids.iterator().hasNext()) { + return emptyList(); + } + + var whereClause = softDeleteData.getWhereClause(includeSoftDeleted); + return list(select("") + + whereClause + (whereClause.isEmpty() ? " WHERE" : " AND") + " e.id IN (:ids)" + + " ORDER BY e.id DESC", p -> p.put("ids", ids)); + } + + /** + * Check whether given entity exists. + * This method supports proxied entities. + * @param entity Entity to check. + * @return Whether entity with given entity exists. + */ + protected boolean exists(E entity) { + var id = getProvider().getIdentifier(entity); + return id != null && createLongQuery("SELECT COUNT(e) FROM " + entityType.getSimpleName() + " e WHERE e.id = :id") + .setParameter("id", id) + .getSingleResult().intValue() > 0; + } + + /** + * List all entities. The default ordering is by ID, descending. This does not include soft deleted entities. + * @return List of all entities. + */ + public List list() { + return list(false); + } + + /** + * List all entities and set whether it may include soft deleted ones. The default ordering is by ID, descending. + * @param includeSoftDeleted Whether to include soft deleted ones in the search. + * @return List of all entities, optionally including soft deleted ones. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + */ + protected List list(boolean includeSoftDeleted) { + return list(select("") + + softDeleteData.getWhereClause(includeSoftDeleted) + + " ORDER BY e.id DESC"); + } + + /** + * List all entities that have been soft deleted. The default ordering is by ID, descending. + * @return List of all soft deleted entities. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + */ + public List listSoftDeleted() { + softDeleteData.checkSoftDeletable(); + return list(select("") + + softDeleteData.getWhereClause(true) + + " ORDER BY e.id DESC"); + } + + /** + * List entities matching the given query and positional parameters, if any. + *

+ * Usage example: + *

+     * List<Foo> foos = list("SELECT f FROM Foo f WHERE f.bar = ?1 AND f.baz = ?2", bar, baz);
+     * 
+ *

+ * Short jpql is also supported: + *

+     * List<Foo> foos = list("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 List of entities matching the given query and positional parameters, if any. + */ + protected List list(String jpql, Object... parameters) { + return createQuery(select(jpql), parameters).getResultList(); + } + + /** + * List entities matching the given query and mapped parameters, if any. + *

+ * Usage example: + *

+     * List<Foo> foos = list("SELECT f FROM Foo f WHERE f.bar = :bar AND f.baz = :baz", params -> {
+     *     params.put("bar", bar);
+     *     params.put("baz", baz);
+     * });
+     * 
+ *

+ * Short jpql is also supported: + *

+     * List<Foo> foos = list("WHERE bar = :bar AND 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 List of entities matching the given query and mapped parameters, if any. + */ + protected List list(String jpql, Consumer> parameters) { + return createQuery(select(jpql), parameters).getResultList(); + } + + /** + * List entities matching the {@link CriteriaQueryBuilder} and mapped parameters, if any. + *

+ * Usage example: + *

+     * List<Foo> foo = list(
+     *         (criteriaBuilder, query, root) -> {
+     *             query.where(criteriaBuilder.equal(root.get("type"), criteriaBuilder.parameter(Type.class, "foo"));
+     *         },
+     *         params -> {
+     *             params.put("foo", Type.FOO);
+     *         }
+     * );
+     * 
+ * @param queryBuilder This creates the JPA criteria query. + * @param parameters To put the mapped query parameters in. + * @return List of entities matching the {@link CriteriaQueryBuilder} and mapped parameters, if any. + */ + protected List list(CriteriaQueryBuilder queryBuilder, Consumer> parameters) { + return createQuery(queryBuilder, parameters).getResultList(); + } + + private String select(String jpql) { + if (!startsWithOneOf(jpql.trim().toLowerCase(), "select", "from")) { + return "SELECT e FROM " + entityType.getSimpleName() + " e " + jpql; + } + else { + return jpql; + } + } + + private String update(String jpql) { + if (!jpql.trim().toLowerCase().startsWith("update")) { + return "UPDATE " + entityType.getSimpleName() + " e " + jpql; + } + else { + return jpql; + } + } + + private TypedQuery createQuery(String jpql, Object... parameters) { + var query = getEntityManager().createQuery(jpql, entityType); + setPositionalParameters(query, parameters); + return query; + } + + private TypedQuery createQuery(String jpql, Consumer> parameters) { + var query = getEntityManager().createQuery(jpql, entityType); + setSuppliedParameters(query, parameters); + return query; + } + + private TypedQuery createQuery(CriteriaQueryBuilder queryBuilder, Consumer> parameters) { + var criteriaBuilder = getEntityManager().getCriteriaBuilder(); + var criteriaQuery = criteriaBuilder.createQuery(entityType); + var root = buildRoot(criteriaQuery); + + queryBuilder.build(criteriaBuilder, criteriaQuery, root); + + var query = getEntityManager().createQuery(criteriaQuery); + + if (root instanceof EclipseLinkRoot eclipseLinkRoot) { + eclipseLinkRoot.runPostponedFetches(query); + } + + setSuppliedParameters(query, parameters); + return query; + } + + + // Insert actions ------------------------------------------------------------------------------------------------- + + /** + * Persist given entity and immediately perform a flush. + * Any bean validation constraint violation will be logged separately. + * @param entity Entity to persist. + * @return Entity ID. + * @throws IllegalEntityStateException When entity is already persisted or its ID is not generated. + */ + public I persist(E entity) { + if (entity.getId() != null) { + if (generatedId || exists(entity)) { + throw new IllegalEntityStateException(entity, "Entity is already persisted. Use update() instead."); + } + } + else if (!generatedId) { + throw new IllegalEntityStateException(entity, "Entity has no generated ID. You need to manually set it."); + } + + try { + getEntityManager().persist(entity); + + } + catch (ConstraintViolationException e) { + logConstraintViolations(e.getConstraintViolations()); + throw e; + } + + // Entity is not guaranteed to have been given an ID before either the TX commits or flush is called. + getEntityManager().flush(); + + return entity.getId(); + } + + + // Update actions ------------------------------------------------------------------------------------------------- + + /** + * Update given entity. If jakarta.persistence.validation.mode property in persistence.xml is explicitly set + * to CALLBACK (and thus not to its default of AUTO), then any bean validation constraint violation will be + * logged separately. Due to technical limitations, this effectively means that bean validation is invoked twice. First in this method + * in order to be able to obtain the constraint violations and then once more while JTA is committing the transaction, but is executed + * beyond the scope of this method. + * @param entity Entity to update. + * @return Updated entity. + * @throws IllegalEntityStateException When entity is not persisted or its ID is not generated. + */ + public E update(E entity) { + if (entity.getId() == null) { + if (generatedId) { + throw new IllegalEntityStateException(entity, "Entity is not persisted. Use persist() instead."); + } + else { + throw new IllegalEntityStateException(entity, "Entity has no generated ID. You need to manually set it."); + } + } + + if (!exists(entity)) { + throw new IllegalEntityStateException(entity, "Entity is not persisted. Use persist() instead."); + } + + if (validator != null) { + // EntityManager#merge() doesn't directly throw ConstraintViolationException without performing flush, so we can't put it in a + // try-catch, and we can't even use an @Interceptor as it happens in JTA side not in EJB side. Hence, we're manually performing + // bean validation here so that we can capture them. + logConstraintViolations(validator.validate(entity)); + } + + return getEntityManager().merge(entity); + } + + /** + * Update given entity via {@link #update(BaseEntity)} and immediately perform a flush so that all changes in + * managed entities so far in the current transaction are persisted. This is particularly useful when you intend + * to process the given entity further in an asynchronous service method. + * @param entity Entity to update. + * @return Updated entity. + * @throws IllegalEntityStateException When entity is not persisted or its ID is not generated. + */ + protected E updateAndFlush(E entity) { + var updatedEntity = update(entity); + getEntityManager().flush(); + return updatedEntity; + } + + private static void logConstraintViolations(Set> constraintViolations) { + constraintViolations.forEach(violation -> { + var constraintName = violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(); + var beanName = violation.getRootBeanClass().getSimpleName(); + var propertyName = violation.getPropertyPath().toString(); + var violationMessage = violation.getMessage(); + Object beanInstance = violation.getRootBean(); + logger.severe(format(LOG_SEVERE_CONSTRAINT_VIOLATION, constraintName, beanName, propertyName, violationMessage, beanInstance)); + }); + } + + /** + * Update given entities. + * @param entities Entities to update. + * @return Updated entities. + * @throws IllegalEntityStateException When at least one entity has no ID. + */ + public List update(Iterable entities) { + return stream(entities).map(this::update).toList(); + } + + /** + * Update or delete all entities matching the given query and positional parameters, if any. + *

+ * Usage example: + *

+     * int affectedRows = update("UPDATE Foo f SET f.bar = ?1 WHERE f.baz = ?2", bar, baz);
+     * 
+ *

+ * Short jpql is also supported: + *

+     * int affectedRows = update("SET bar = ?1 WHERE baz = ?2", bar, baz);
+     * 
+ * @param jpql The Java Persistence Query Language statement. + * @param parameters The positional query parameters, if any. + * @return The number of entities updated or deleted. + * @see Query#executeUpdate() + */ + protected int update(String jpql, Object... parameters) { + return createQuery(update(jpql), parameters).executeUpdate(); + } + + /** + * Update or delete all entities matching the given query and mapped parameters, if any. + *

+ * Usage example: + *

+     * int affectedRows = update("UPDATE Foo f SET f.bar = :bar WHERE f.baz = :baz", params -> {
+     *     params.put("bar", bar);
+     *     params.put("baz", baz);
+     * });
+     * 
+ *

+ * Short jpql is also supported: + *

+     * int affectedRows = update("SET bar = :bar WHERE 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, if any. + * @return The number of entities updated or deleted. + * @see Query#executeUpdate() + */ + protected int update(String jpql, Consumer> parameters) { + return createQuery(update(jpql), parameters).executeUpdate(); + } + + /** + * Save given entity. This will automatically determine based on the presence of generated entity ID, + * or existence of an entity in the data store whether to {@link #persist(BaseEntity)} or to {@link #update(BaseEntity)}. + * @param entity Entity to save. + * @return Saved entity. + */ + public E save(E entity) { + if (generatedId && entity.getId() == null || !generatedId && !exists(entity)) { + persist(entity); + return entity; + } + else { + return update(entity); + } + } + + /** + * Save given entity via {@link #save(BaseEntity)} and immediately perform a flush so that all changes in + * managed entities so far in the current transaction are persisted. This is particularly useful when you intend + * to process the given entity further in an asynchronous service method. + * @param entity Entity to save. + * @return Saved entity. + */ + protected E saveAndFlush(E entity) { + var savedEntity = save(entity); + getEntityManager().flush(); + return savedEntity; + } + + + // Delete actions ------------------------------------------------------------------------------------------------- + + /** + * Delete given entity. + * @param entity Entity to delete. + * @throws NonDeletableEntityException When entity has {@link NonDeletable} annotation set. + * @throws IllegalEntityStateException When entity has no ID. + * @throws EntityNotFoundException When entity has in meanwhile been deleted. + */ + public void delete(E entity) { + if (entity.getClass().isAnnotationPresent(NonDeletable.class)) { + throw new NonDeletableEntityException(entity); + } + + getEntityManager().remove(manage(entity)); + + if (getProvider() != ECLIPSELINK || !getEntityManager().contains(entity)) { + entity.setId(null); + } + } + + /** + * Soft delete given entity. + * @param entity Entity to soft delete. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + * @throws IllegalEntityStateException When entity has no ID. + * @throws EntityNotFoundException When entity has in meanwhile been hard deleted. + */ + public void softDelete(E entity) { + softDeleteData.checkSoftDeletable(); + softDeleteData.setSoftDeleted(manage(entity), true); + } + + /** + * Soft undelete given entity. + * @param entity Entity to soft undelete. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + * @throws IllegalEntityStateException When entity has no ID. + * @throws EntityNotFoundException When entity has in meanwhile been hard deleted. + */ + public void softUndelete(E entity) { + softDeleteData.checkSoftDeletable(); + softDeleteData.setSoftDeleted(manage(entity), false); + } + + /** + * Delete given entities. + * @param entities Entities to delete. + * @throws NonDeletableEntityException When at least one entity has {@link NonDeletable} annotation set. + * @throws IllegalEntityStateException When at least one entity has no ID. + * @throws EntityNotFoundException When at least one entity has in meanwhile been deleted. + */ + public void delete(Iterable entities) { + entities.forEach(this::delete); + } + + /** + * Soft delete given entities. + * @param entities Entities to soft delete. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + * @throws IllegalEntityStateException When at least one entity has no ID. + * @throws EntityNotFoundException When at least one entity has in meanwhile been hard deleted. + */ + public void softDelete(Iterable entities) { + entities.forEach(this::softDelete); + } + + /** + * Soft undelete given entities. + * @param entities Entities to soft undelete. + * @throws NonSoftDeletableEntityException When entity doesn't have {@link SoftDeletable} annotation set on any of its fields. + * @throws IllegalEntityStateException When at least one entity has no ID. + * @throws EntityNotFoundException When at least one entity has in meanwhile been hard deleted. + */ + public void softUndelete(Iterable entities) { + entities.forEach(this::softUndelete); + } + + + // Manage actions ------------------------------------------------------------------------------------------------- + + /** + * Make given entity managed. NOTE: This will discard any unmanaged changes in the given entity! + * This is particularly useful in case you intend to make sure that you have the most recent version at hands. + * This method also supports proxied entities as well as DTOs. + * @param entity Entity to manage. + * @return The managed entity. + * @throws NullPointerException When given entity is null. + * @throws IllegalEntityStateException When entity has no ID. + * @throws EntityNotFoundException When entity has in meanwhile been deleted. + */ + protected E manage(E entity) { + if (entity == null) { + throw new NullPointerException("Entity is null."); + } + + var id = getProvider().getIdentifier(entity); + + if (id == null) { + throw new IllegalEntityStateException(entity, "Entity has no ID."); + } + + if (entity.getClass().getAnnotation(Entity.class) != null && getEntityManager().contains(entity)) { + return entity; + } + + var managed = getEntityManager().find(getProvider().getEntityType(entity), id); + + if (managed == null) { + throw new EntityNotFoundException("Entity has in meanwhile been deleted."); + } + + return managed; + } + + /** + * Make any given entity managed if necessary. NOTE: This will discard any unmanaged changes in the given entity! + * This is particularly useful in case you intend to make sure that you have the most recent version at hands. + * This method also supports null entities as well as proxied entities as well as DTOs. + * @param The generic entity type. + * @param entity Entity to manage, may be null. + * @return The managed entity, or null when null was supplied. + * It leniently returns the very same argument if the entity has no ID or has been deleted in the meanwhile. + * @throws IllegalArgumentException When the given entity is actually not an instance of {@link BaseEntity}. + */ + @SuppressWarnings({ "hiding", "unchecked" }) + protected E manageIfNecessary(E entity) { + if (entity == null) { + return null; + } + + if (!(entity instanceof BaseEntity baseEntity)) { + throw new IllegalArgumentException(); + } + + var id = getProvider().getIdentifier(baseEntity); + + if (id == null || entity.getClass().getAnnotation(Entity.class) != null && getEntityManager().contains(entity)) { + return entity; + } + + return coalesce((E) getEntityManager().find(getProvider().getEntityType(baseEntity), id), entity); + } + + /** + * Reset given entity. This will discard any changes in given entity. The given entity must be unmanaged/detached. + * The actual intent of this method is to have the opportunity to completely reset the state of a given entity + * which might have been edited in the client, without changing the reference. This is generally useful when the + * entity is in turn held in some collection and you'd rather not manually remove and reinsert it in the collection. + * This method supports proxied entities. + * @param entity Entity to reset. + * @throws IllegalEntityStateException When entity is already managed, or has no ID. + * @throws EntityNotFoundException When entity has in meanwhile been deleted. + */ + public void reset(E entity) { + if (!getProvider().isProxy(entity) && getEntityManager().contains(entity)) { + throw new IllegalEntityStateException(entity, "Only unmanaged entities can be resetted."); + } + + var managed = manage(entity); + getMetamodel(entity).getAttributes().stream().map(Attribute::getJavaMember).filter(Field.class::isInstance).forEach(field -> map(field, managed, entity)); + // Note: EntityManager#refresh() is insuitable as it requires a managed entity and thus merge() could unintentionally persist changes before resetting. + } + + + // Count actions -------------------------------------------------------------------------------------------------- + + /** + * Returns count of all foreign key references to given entity. + * This is particularly useful in case you intend to check if the given entity is still referenced elsewhere in database. + * @param entity Entity to count all foreign key references for. + * @return Count of all foreign key references to given entity. + */ + protected long countForeignKeyReferencesTo(E entity) { + return countForeignKeyReferences(getEntityManager(), entityType, identifierType, manage(entity).getId()); + } + + + // Lazy fetching actions ------------------------------------------------------------------------------------------ + + /** + * Fetch lazy collections of given entity on given getters. If no getters are supplied, then it will fetch every + * single {@link PluralAttribute} not of type {@link CollectionType#MAP}. + * Note that the implementation does for simplicitly not check if those are actually lazy or eager. + *

+ * Usage example: + *

+     * Foo fooWithBarsAndBazs = fetchLazyCollections(getById(fooId), Foo::getBars, Foo::getBazs);
+     * 
+ * @param entity Entity instance to fetch lazy collections on. + * @param getters Getters of those lazy collections. + * @return The same entity, useful if you want to continue using it immediately. + */ + @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. + protected E fetchLazyCollections(E entity, Function>... getters) { + return fetchPluralAttributes(entity, type -> type != MAP, getters); + } + + /** + * Fetch lazy collections of given optional entity on given getters. If no getters are supplied, then it will fetch + * every single {@link PluralAttribute} not of type {@link CollectionType#MAP}. + * Note that the implementation does for simplicitly not check if those are actually lazy or eager. + *

+ * Usage example: + *

+     * Optional<Foo> fooWithBarsAndBazs = fetchLazyCollections(findById(fooId), Foo::getBars, Foo::getBazs);
+     * 
+ * @param entity Optional entity instance to fetch lazy collections on. + * @param getters Getters of those lazy collections. + * @return The same optional entity, useful if you want to continue using it immediately. + */ + @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. + protected Optional fetchLazyCollections(Optional entity, Function>... getters) { + return ofNullable(entity.isPresent() ? fetchLazyCollections(entity.get(), getters) : null); + } + + /** + * Fetch lazy maps of given entity on given getters. If no getters are supplied, then it will fetch every single + * {@link PluralAttribute} of type {@link CollectionType#MAP}. + * Note that the implementation does for simplicitly not check if those are actually lazy or eager. + *

+ * Usage example: + *

+     * Foo fooWithBarsAndBazs = fetchLazyCollections(getById(fooId), Foo::getBars, Foo::getBazs);
+     * 
+ * @param entity Entity instance to fetch lazy maps on. + * @param getters Getters of those lazy collections. + * @return The same entity, useful if you want to continue using it immediately. + */ + @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. + protected E fetchLazyMaps(E entity, Function>... getters) { + return fetchPluralAttributes(entity, type -> type == MAP, getters); + } + + /** + * Fetch lazy maps of given optional entity on given getters. If no getters are supplied, then it will fetch every + * single {@link PluralAttribute} of type {@link CollectionType#MAP}. + * Note that the implementation does for simplicitly not check if those are actually lazy or eager. + *

+ * Usage example: + *

+     * Optional<Foo> fooWithBarsAndBazs = fetchLazyCollections(findById(fooId), Foo::getBars, Foo::getBazs);
+     * 
+ * @param entity Optional entity instance to fetch lazy maps on. + * @param getters Getters of those lazy collections. + * @return The same optional entity, useful if you want to continue using it immediately. + */ + @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. + protected Optional fetchLazyMaps(Optional entity, Function>... getters) { + return ofNullable(entity.isPresent() ? fetchLazyMaps(entity.get(), getters) : null); + } + + /** + * Fetch all lazy blobs of given entity. + * Note that the implementation does for simplicitly not check if those are actually lazy or eager. + * @param entity Entity instance to fetch all blobs on. + * @return The same entity, useful if you want to continue using it immediately. + */ + protected E fetchLazyBlobs(E entity) { + return fetchSingularAttributes(entity, type -> type == byte[].class); + } + + /** + * Fetch all lazy blobs of given optional entity. + * Note that the implementation does for simplicitly not check if those are actually lazy or eager. + * @param entity Optional entity instance to fetch all blobs on. + * @return The same optional entity, useful if you want to continue using it immediately. + */ + protected Optional fetchLazyBlobs(Optional entity) { + return ofNullable(entity.isPresent() ? fetchLazyBlobs(entity.get()) : null); + } + + @SuppressWarnings("unchecked") // Unfortunately, @SafeVarargs cannot be used as it requires a final method. + private E fetchPluralAttributes(E entity, java.util.function.Predicate ofType, Function... getters) { + if (isEmpty(getters)) { + for (var a : getMetamodel().getPluralAttributes()) { + if (ofType.test(a.getCollectionType())) { + ofNullable(invokeGetter(entity, a.getName())).ifPresent(c -> invokeMethod(c, "size")); + } + } + } + else { + stream(getters).forEach(getter -> ofNullable(getter.apply(entity)).ifPresent(c -> invokeMethod(c, "size"))); + } + + return entity; + } + + private E fetchSingularAttributes(E entity, java.util.function.Predicate> ofType) { + var managed = getById(entity.getId()); + + for (var a : getMetamodel().getSingularAttributes()) { + if (ofType.test(a.getJavaType())) { + var name = capitalize(a.getName()); + invokeSetter(entity, name, invokeGetter(managed, name)); + } + } + + return entity; + } + + + // Paging actions ------------------------------------------------------------------------------------------------- + + /** + * Functional interface to fine-grain a JPA criteria query for any of {@link #getPage(Page, boolean)} methods. + *

+ * You do not need this interface directly. Just supply a lambda. Below is an usage example: + *

+     * @Stateless
+     * public class YourEntityService extends BaseEntityService<YourEntity> {
+     *
+     *     public void getPageOfFooType(Page page, boolean count) {
+     *         return getPage(page, count, (criteriaBuilder, query, root) -> {
+     *             query.where(criteriaBuilder.equal(root.get("type"), Type.FOO));
+     *         });
+     *     }
+     *
+     * }
+     * 
+ * @param The generic base entity type. + */ + @FunctionalInterface + protected interface QueryBuilder { + void build(CriteriaBuilder criteriaBuilder, AbstractQuery query, Root root); + } + + /** + * Functional interface to fine-grain a JPA criteria query for any of {@link #getPage(Page, boolean)} methods taking + * a specific result type, such as an entity subclass (DTO). You must return a {@link LinkedHashMap} with + * {@link Getter} as key and {@link Expression} as value. The mapping must be in exactly the same order as + * constructor arguments of your DTO. + *

+ * You do not need this interface directly. Just supply a lambda. Below is an usage example: + *

+     * public class YourEntityDTO extends YourEntity {
+     *
+     *     private BigDecimal totalPrice;
+     *
+     *     public YourEntityDTO(Long id, String name, BigDecimal totalPrice) {
+     *         setId(id);
+     *         setName(name);
+     *         this.totalPrice = totalPrice;
+     *     }
+     *
+     *     public BigDecimal getTotalPrice() {
+     *         return totalPrice;
+     *     }
+     *
+     * }
+     * 
+ *
+     * @Stateless
+     * public class YourEntityService extends BaseEntityService<YourEntity> {
+     *
+     *     public void getPageOfYourEntityDTO(Page page, boolean count) {
+     *         return getPage(page, count, YourEntityDTO.class (criteriaBuilder, query, root) -> {
+     *             Join<YourEntityDTO, YourChildEntity> child = root.join("child");
+     *
+     *             LinkedHashMap<Getter<YourEntityDTO>, Expression<?>> mapping = new LinkedHashMap<>();
+     *             mapping.put(YourEntityDTO::getId, root.get("id"));
+     *             mapping.put(YourEntityDTO::getName, root.get("name"));
+     *             mapping.put(YourEntityDTO::getTotalPrice, builder.sum(child.get("price")));
+     *
+     *             return mapping;
+     *         });
+     *     }
+     *
+     * }
+     * 
+ * @param The generic base entity type or from a DTO subclass thereof. + */ + @FunctionalInterface + protected interface MappedQueryBuilder { + LinkedHashMap, Expression> build(CriteriaBuilder criteriaBuilder, AbstractQuery query, Root root); + } + + /** + * Here you can in your {@link BaseEntityService} subclass define the callback method which needs to be invoked before any of + * {@link #getPage(Page, boolean)} methods is called. For example, to set a vendor specific {@link EntityManager} hint. + * The default implementation returns a no-op callback. + * @return The callback method which is invoked before any of {@link #getPage(Page, boolean)} methods is called. + */ + protected Consumer beforePage() { + return entityManager -> noop(); + } + + /** + * Here you can in your {@link BaseEntityService} subclass define the callback method which needs to be invoked when any query involved in + * {@link #getPage(Page, boolean)} is about to be executed. For example, to set a vendor specific {@link Query} hint. + * The default implementation sets Hibernate, EclipseLink and JPA 2.0 cache-related hints. When cacheable argument is + * true, then it reads from cache where applicable, else it will read from DB and force a refresh of cache. Note that + * this is not supported by OpenJPA. + * @param The generic type of the entity or a DTO subclass thereof. + * @param resultType The result type which can be the entity type itself or a DTO subclass thereof. + * @param cacheable Whether the results should be cacheable. + * @return The callback method which is invoked when any query involved in {@link #getPage(Page, boolean)} is about + * to be executed. + */ + protected Consumer> onPage(Class resultType, boolean cacheable) { + return typedQuery -> { + if (getProvider() == HIBERNATE) { + typedQuery + .setHint(QUERY_HINT_HIBERNATE_CACHEABLE, cacheable); + } + else if (getProvider() == ECLIPSELINK) { + typedQuery + .setHint(QUERY_HINT_ECLIPSELINK_MAINTAIN_CACHE, cacheable) + .setHint(QUERY_HINT_ECLIPSELINK_REFRESH, !cacheable); + } + + if (getProvider() != OPENJPA) { + // OpenJPA doesn't support 2nd level cache. + typedQuery + .setHint(QUERY_HINT_CACHE_STORE_MODE, cacheable ? CacheStoreMode.USE : CacheStoreMode.REFRESH) + .setHint(QUERY_HINT_CACHE_RETRIEVE_MODE, cacheable ? CacheRetrieveMode.USE : CacheRetrieveMode.BYPASS); + } + }; + } + + /** + * Here you can in your {@link BaseEntityService} subclass define the callback method which needs to be invoked after any of + * {@link #getPage(Page, boolean)} methods is called. For example, to remove a vendor specific {@link EntityManager} hint. + * The default implementation returns a no-op callback. + * @return The callback method which is invoked after any of {@link #getPage(Page, boolean)} methods is called. + */ + protected Consumer afterPage() { + return entityManager -> noop(); + } + + /** + * Returns a partial result list based on given {@link Page}. This will by default cache the results. + *

+ * Usage examples: + *

+     * Page first10Records = Page.of(0, 10);
+     * PartialResultList<Foo> foos = getPage(first10Records, true);
+     * 
+ *
+     * Map<String, Object> criteria = new HashMap<>();
+     * criteria.put("bar", bar); // Exact match.
+     * criteria.put("baz", IgnoreCase.value(baz)); // Case insensitive match.
+     * criteria.put("faz", Like.contains(faz)); // Case insensitive LIKE match.
+     * criteria.put("kaz", Order.greaterThan(kaz)); // Greater than match.
+     * Page first10RecordsMatchingCriteriaOrderedByBar = Page.with().allMatch(criteria).orderBy("bar", true).range(0, 10);
+     * PartialResultList<Foo> foos = getPage(first10RecordsMatchingCriteriaOrderedByBar, true);
+     * 
+ * @param page The page to return a partial result list for. + * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be + * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. + * @return A partial result list based on given {@link Page}. + * @see Page + * @see Criteria + */ + public PartialResultList getPage(Page page, boolean count) { + // Implementation notice: we can't remove this getPage() method and rely on the other getPage() method with varargs below, + // because the one with varargs is incompatible as method reference for getPage(Page, boolean) in some Java versions. + // See https://github.com/omnifaces/omnipersistence/issues/11 + return getPage(page, count, true, entityType, (builder, query, root) -> noop()); + } + + /** + * Returns a partial result list based on given {@link Page} and fetch fields. This will by default cache the results. + *

+ * Usage examples: + *

+     * Page first10Records = Page.of(0, 10);
+     * PartialResultList<Foo> foosWithBars = getPage(first10Records, true, "bar");
+     * 
+ *
+     * Map<String, Object> criteria = new HashMap<>();
+     * criteria.put("bar", bar); // Exact match.
+     * criteria.put("baz", IgnoreCase.value(baz)); // Case insensitive match.
+     * criteria.put("faz", Like.contains(faz)); // Case insensitive LIKE match.
+     * criteria.put("kaz", Order.greaterThan(kaz)); // Greater than match.
+     * Page first10RecordsMatchingCriteriaOrderedByBar = Page.with().allMatch(criteria).orderBy("bar", true).range(0, 10);
+     * PartialResultList<Foo> foosWithBars = getPage(first10RecordsMatchingCriteriaOrderedByBar, true, "bar");
+     * 
+ * @param page The page to return a partial result list for. + * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be + * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. + * @param fetchFields Optionally, all (lazy loaded) fields to be explicitly fetched during the query. Each field + * can represent a JavaBean path, like as you would do in EL, such as parent.child.subchild. + * @return A partial result list based on given {@link Page}. + * @see Page + * @see Criteria + */ + protected PartialResultList getPage(Page page, boolean count, String... fetchFields) { + return getPage(page, count, true, fetchFields); + } + + /** + * Returns a partial result list based on given {@link Page} with the option whether to cache the results or not. + *

+ * Usage example: see {@link #getPage(Page, boolean)} and {@link #getPage(Page, boolean, String...)}. + * @param page The page to return a partial result list for. + * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be + * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. + * @param cacheable Whether the results should be cacheable. + * @param fetchFields Optionally, all (lazy loaded) fields to be explicitly fetched during the query. Each field + * can represent a JavaBean path, like as you would do in EL, such as parent.child.subchild. + * @return A partial result list based on given {@link Page}. + * @see Page + * @see Criteria + */ + protected PartialResultList getPage(Page page, boolean count, boolean cacheable, String... fetchFields) { + return getPage(page, count, cacheable, entityType, (builder, query, root) -> { + for (var fetchField : fetchFields) { + FetchParent fetchParent = root; + + for (var attribute : fetchField.split("\\.")) { + fetchParent = fetchParent.fetch(attribute); + } + } + + return null; + }); + } + + /** + * Returns a partial result list based on given {@link Page} and {@link QueryBuilder}. This will by default cache + * the results. + *

+ * Usage example: see {@link QueryBuilder}. + * @param page The page to return a partial result list for. + * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be + * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. + * @param queryBuilder This allows fine-graining the JPA criteria query. + * @return A partial result list based on given {@link Page} and {@link QueryBuilder}. + * @see Page + * @see Criteria + */ + protected PartialResultList getPage(Page page, boolean count, QueryBuilder queryBuilder) { + return getPage(page, count, true, queryBuilder); + } + + /** + * Returns a partial result list based on given {@link Page}, entity type and {@link QueryBuilder} with the option + * whether to cache the results or not. + *

+ * Usage example: see {@link QueryBuilder}. + * @param page The page to return a partial result list for. + * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be + * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. + * @param cacheable Whether the results should be cacheable. + * @param queryBuilder This allows fine-graining the JPA criteria query. + * @return A partial result list based on given {@link Page} and {@link QueryBuilder}. + * @see Page + * @see Criteria + */ + @SuppressWarnings("unchecked") + protected PartialResultList getPage(Page page, boolean count, boolean cacheable, QueryBuilder queryBuilder) { + return getPage(page, count, cacheable, entityType, (builder, query, root) -> { + queryBuilder.build(builder, query, (Root) root); + return new LinkedHashMap<>(0); + }); + } + + /** + * Returns a partial result list based on given {@link Page}, result type and {@link MappedQueryBuilder}. This will + * by default cache the results. + *

+ * Usage example: see {@link MappedQueryBuilder}. + * @param The generic type of the entity or a DTO subclass thereof. + * @param page The page to return a partial result list for. + * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be + * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. + * @param resultType The result type which can be the entity type itself or a DTO subclass thereof. + * @param mappedQueryBuilder This allows fine-graining the JPA criteria query and must return a mapping of + * getters-paths. + * @return A partial result list based on given {@link Page} and {@link MappedQueryBuilder}. + * @throws IllegalArgumentException When the result type does not equal entity type and mapping is empty. + * @see Page + * @see Criteria + */ + protected PartialResultList getPage(Page page, boolean count, Class resultType, MappedQueryBuilder mappedQueryBuilder) { + return getPage(page, count, true, resultType, mappedQueryBuilder); + } + + /** + * Returns a partial result list based on given {@link Page}, entity type and {@link QueryBuilder} with the option + * whether to cache the results or not. + *

+ * Usage example: see {@link MappedQueryBuilder}. + * @param The generic type of the entity or a DTO subclass thereof. + * @param page The page to return a partial result list for. + * @param count Whether to run the COUNT(id) query to estimate total number of results. This will be + * available by {@link PartialResultList#getEstimatedTotalNumberOfResults()}. + * @param cacheable Whether the results should be cacheable. + * @param resultType The result type which can be the entity type itself or a DTO subclass thereof. + * @param queryBuilder This allows fine-graining the JPA criteria query and must return a mapping of + * getters-paths when result type does not equal entity type. + * @return A partial result list based on given {@link Page} and {@link MappedQueryBuilder}. + * @throws IllegalArgumentException When the result type does not equal entity type and mapping is empty. + * @see Page + * @see Criteria + */ + protected PartialResultList getPage(Page page, boolean count, boolean cacheable, Class resultType, MappedQueryBuilder queryBuilder) { + beforePage().accept(getEntityManager()); + + try { + logger.log(FINER, () -> format(LOG_FINER_GET_PAGE, page, count, cacheable, resultType)); + var pageBuilder = new PageBuilder<>(page, cacheable, resultType, queryBuilder); + var criteriaBuilder = getEntityManager().getCriteriaBuilder(); + TypedQuery entityQuery = buildEntityQuery(pageBuilder, criteriaBuilder); + var countQuery = count ? buildCountQuery(pageBuilder, criteriaBuilder) : null; + PartialResultList resultList = executeQuery(pageBuilder, entityQuery, countQuery); + logger.log(FINER, () -> format(LOG_FINER_QUERY_RESULT, resultList, resultList.getEstimatedTotalNumberOfResults())); + return resultList; + } + finally { + afterPage().accept(getEntityManager()); + } + } + + + // Query actions -------------------------------------------------------------------------------------------------- + + private TypedQuery buildEntityQuery(PageBuilder pageBuilder, CriteriaBuilder criteriaBuilder) { + var entityQuery = criteriaBuilder.createQuery(pageBuilder.getResultType()); + var entityQueryRoot = buildRoot(entityQuery); + var pathResolver = buildSelection(pageBuilder, entityQuery, entityQueryRoot, criteriaBuilder); + buildOrderBy(pageBuilder, entityQuery, criteriaBuilder, pathResolver); + return buildTypedQuery(pageBuilder, entityQuery, entityQueryRoot, buildRestrictions(pageBuilder, entityQuery, criteriaBuilder, pathResolver)); + } + + private TypedQuery buildCountQuery(PageBuilder pageBuilder, CriteriaBuilder criteriaBuilder) { + var countQuery = criteriaBuilder.createQuery(Long.class); + var countQueryRoot = countQuery.from(entityType); + countQuery.select(criteriaBuilder.count(countQueryRoot)); var parameters = pageBuilder.shouldBuildCountSubquery() ? buildCountSubquery(pageBuilder, countQuery, countQueryRoot, criteriaBuilder) : Collections.emptyMap(); - return buildTypedQuery(pageBuilder, countQuery, null, parameters); - } - - private Map buildCountSubquery(PageBuilder pageBuilder, CriteriaQuery countQuery, Root countRoot, CriteriaBuilder criteriaBuilder) { - var countSubquery = countQuery.subquery(pageBuilder.getResultType()); - var countSubqueryRoot = buildRoot(countSubquery); - var subqueryPathResolver = buildSelection(pageBuilder, countSubquery, countSubqueryRoot, criteriaBuilder); - var parameters = buildRestrictions(pageBuilder, countSubquery, criteriaBuilder, subqueryPathResolver); - - // SELECT COUNT(e) FROM E e WHERE EXISTS (SELECT t.id FROM T t WHERE [restrictions] AND t.id = e.id) - countQuery.where(criteriaBuilder.exists(countSubquery.where(conjunctRestrictionsIfNecessary(criteriaBuilder, countSubquery.getRestriction(), criteriaBuilder.equal(countSubqueryRoot.get(ID), countRoot.get(ID)))))); - - return parameters; - } - - private TypedQuery buildTypedQuery(PageBuilder pageBuilder, CriteriaQuery criteriaQuery, Root root, Map parameters) { - var typedQuery = getEntityManager().createQuery(criteriaQuery); - buildRange(pageBuilder, typedQuery, root); - setMappedParameters(typedQuery, parameters); - onPage(pageBuilder.getResultType(), pageBuilder.isCacheable()).accept(typedQuery); - return typedQuery; - } - - private static void setPositionalParameters(TypedQuery typedQuery, Object[] positionalParameters) { - logger.log(FINER, () -> format(LOG_FINER_SET_PARAMETER_VALUES, Arrays.toString(positionalParameters))); - range(0, positionalParameters.length).forEach(i -> typedQuery.setParameter(i, positionalParameters[i])); - } - - private static void setMappedParameters(TypedQuery typedQuery, Map mappedParameters) { - logger.log(FINER, () -> format(LOG_FINER_SET_PARAMETER_VALUES, mappedParameters)); - mappedParameters.entrySet().forEach(parameter -> typedQuery.setParameter(parameter.getKey(), parameter.getValue())); - } - - private static void setSuppliedParameters(TypedQuery typedQuery, Consumer> suppliedParameters) { - var mappedParameters = new HashMap(); - suppliedParameters.accept(mappedParameters); - setMappedParameters(typedQuery, mappedParameters); - } - - private PartialResultList executeQuery(PageBuilder pageBuilder, TypedQuery entityQuery, TypedQuery countQuery) { - var page = pageBuilder.getPage(); - var entities = entityQuery.getResultList(); - - if (pageBuilder.canBuildValueBasedPagingPredicate() && page.isReversed()) { - var reversed = new ArrayList<>(entities); - Collections.reverse(reversed); - entities = reversed; - } - - var estimatedTotalNumberOfResults = countQuery != null ? countQuery.getSingleResult().intValue() : -1; - return new PartialResultList<>(entities, page.getOffset(), estimatedTotalNumberOfResults); - } - - - // Selection actions ---------------------------------------------------------------------------------------------- - - private Root buildRoot(AbstractQuery query) { - var root = query.from(entityType); - return query instanceof Subquery ? new SubqueryRoot<>(root) : getProvider() == ECLIPSELINK ? new EclipseLinkRoot<>(root) : root; - } - - private PathResolver buildSelection(PageBuilder pageBuilder, AbstractQuery query, Root root, CriteriaBuilder criteriaBuilder) { - var mapping = pageBuilder.getQueryBuilder().build(criteriaBuilder, query, root); - - if (query instanceof Subquery subQuery) { - subQuery.select(root.get(ID)); - } - - if (!isEmpty(mapping)) { // mapping is not empty when getPage(..., MappedQueryBuilder) is used. - Map> paths = stream(mapping).collect(toMap(e -> e.getKey().getPropertyName(), Entry::getValue, (l, r) -> l, LinkedHashMap::new)); - - if (query instanceof CriteriaQuery criteriaQuery) { - criteriaQuery.multiselect(stream(paths).map(Alias::as).collect(toList())); - } - - Set aggregatedFields = paths.entrySet().stream().filter(e -> getProvider().isAggregation(e.getValue())).map(Entry::getKey).collect(toSet()); - - if (!aggregatedFields.isEmpty()) { - groupByIfNecessary(query, root); - } - - var orderingContainsAggregatedFields = aggregatedFields.removeAll(pageBuilder.getPage().getOrdering().keySet()); - pageBuilder.shouldBuildCountSubquery(true); // Normally, building of count subquery is skipped for performance, but when there's a custom mapping, we cannot reliably determine if custom criteria is used, so count subquery building cannot be reliably skipped. - pageBuilder.canBuildValueBasedPagingPredicate(getProvider() != HIBERNATE || !orderingContainsAggregatedFields); // Value based paging cannot be used in Hibernate if ordering contains aggregated fields, because Hibernate may return a cartesian product and apply firstResult/maxResults in memory. - return new MappedPathResolver(root, paths, elementCollections.get(), manyOrOneToOnes.get()); - } - else if (pageBuilder.getResultType() == entityType) { - pageBuilder.shouldBuildCountSubquery(mapping != null); // mapping is empty but not null when getPage(..., QueryBuilder) is used. - pageBuilder.canBuildValueBasedPagingPredicate(mapping == null); // when mapping is not null, we cannot reliably determine if ordering contains aggregated fields, so value based paging cannot be reliably used. - return new RootPathResolver(root, elementCollections.get(), manyOrOneToOnes.get()); - } - else { - throw new IllegalArgumentException(ERROR_ILLEGAL_MAPPING); - } - } - - private void buildRange(PageBuilder pageBuilder, Query query, Root root) { - if (root == null) { - return; - } - - var hasJoins = hasJoins(root); - var page = pageBuilder.getPage(); - - if ((hasJoins || page.getOffset() > 0) && !pageBuilder.canBuildValueBasedPagingPredicate()) { - query.setFirstResult(page.getOffset()); - } - - if (hasJoins || page.getLimit() != MAX_VALUE) { - query.setMaxResults(page.getLimit()); - } - - if (hasJoins && root instanceof EclipseLinkRoot eclipseLinkRoot) { - eclipseLinkRoot.runPostponedFetches(query); - } - } - - - // Sorting actions ------------------------------------------------------------------------------------------------ - - private void buildOrderBy(PageBuilder pageBuilder, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder, PathResolver pathResolver) { - var page = pageBuilder.getPage(); - var ordering = page.getOrdering(); - - if (ordering.isEmpty() || page.getLimit() - page.getOffset() == 1) { - return; - } - - var reversed = pageBuilder.canBuildValueBasedPagingPredicate() && page.isReversed(); - criteriaQuery.orderBy(stream(ordering).map(order -> buildOrder(order, criteriaBuilder, pathResolver, reversed)).collect(toList())); - } - - private Order buildOrder(Entry order, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, boolean reversed) { - var field = order.getKey(); - - if (oneToManys.test(field) || elementCollections.get().contains(field)) { - if (getProvider() == ECLIPSELINK) { - throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ONETOMANY_ORDERBY_ECLIPSELINK); // EclipseLink refuses to perform a JOIN when setFirstResult/setMaxResults is used. - } - else if (getProvider() == OPENJPA) { - throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ONETOMANY_ORDERBY_OPENJPA); // OpenJPA adds for some reason a second JOIN on the join table referenced in ORDER BY column causing it to be not sorted on the intended join. - } - } - - var path = pathResolver.get(field); - return order.getValue() ^ reversed ? criteriaBuilder.asc(path) : criteriaBuilder.desc(path); - } - - - // Searching actions ----------------------------------------------------------------------------------------------- - - private Map buildRestrictions(PageBuilder pageBuilder, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver) { - var page = pageBuilder.getPage(); - var parameters = new HashMap(page.getRequiredCriteria().size() + page.getOptionalCriteria().size()); - var requiredPredicates = buildPredicates(page.getRequiredCriteria(), query, criteriaBuilder, pathResolver, parameters); - var optionalPredicates = buildPredicates(page.getOptionalCriteria(), query, criteriaBuilder, pathResolver, parameters); - Predicate restriction = null; - - if (!optionalPredicates.isEmpty()) { - pageBuilder.shouldBuildCountSubquery(true); - restriction = criteriaBuilder.or(toArray(optionalPredicates)); - } - - if (!requiredPredicates.isEmpty()) { - pageBuilder.shouldBuildCountSubquery(true); - var wherePredicates = requiredPredicates.stream().filter(Alias::isWhere).collect(toList()); - - if (!wherePredicates.isEmpty()) { - restriction = conjunctRestrictionsIfNecessary(criteriaBuilder, restriction, wherePredicates); - } - - var inPredicates = wherePredicates.stream().filter(Alias::isIn).collect(toList()); - - for (var inPredicate : inPredicates) { - var countPredicate = buildCountPredicateIfNecessary(inPredicate, criteriaBuilder, query, pathResolver); - - if (countPredicate != null) { - requiredPredicates.add(countPredicate); - } - } - - var havingPredicates = requiredPredicates.stream().filter(Alias::isHaving).collect(toList()); - - if (!havingPredicates.isEmpty()) { - groupByIfNecessary(query, pathResolver.get(null)); - query.having(conjunctRestrictionsIfNecessary(criteriaBuilder, query.getGroupRestriction(), havingPredicates)); - } - } - - if (!(query instanceof Subquery) && pageBuilder.canBuildValueBasedPagingPredicate()) { - restriction = conjunctRestrictionsIfNecessary(criteriaBuilder, restriction, buildValueBasedPagingPredicate(page, criteriaBuilder, pathResolver, parameters)); - } - - if (restriction != null) { - var distinct = !optionalPredicates.isEmpty() || hasFetches((From) pathResolver.get(null)); - query.distinct(distinct).where(conjunctRestrictionsIfNecessary(criteriaBuilder, query.getRestriction(), restriction)); - } - - return parameters; - } - - @SuppressWarnings("unchecked") - private > Predicate buildValueBasedPagingPredicate(Page page, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, Map parameters) { - // Value based paging https://blog.novatec-gmbh.de/art-pagination-offset-vs-value-based-paging/ is on large offsets much faster than offset based paging. - // (orderByField1 > ?1) OR (orderByField1 = ?1 AND orderByField2 > ?2) OR (orderByField1 = ?1 AND orderByField2 = ?2 AND orderByField3 > ?3) [...] - - var predicates = new ArrayList(page.getOrdering().size()); - var orderByFields = new HashMap, ParameterExpression>(); - var last = (T) page.getLast(); - - for (var order : page.getOrdering().entrySet()) { - var field = order.getKey(); - var value = invokeGetter(last, field); - var path = (Expression) pathResolver.get(field); - ParameterExpression parameter = new UncheckedParameterBuilder(field, criteriaBuilder, parameters).create(value); - var predicate = order.getValue() ^ page.isReversed() ? criteriaBuilder.greaterThan(path, parameter) : criteriaBuilder.lessThan(path, parameter); - - for (var previousOrderByField : orderByFields.entrySet()) { - var previousPath = previousOrderByField.getKey(); - var previousParameter = previousOrderByField.getValue(); - predicate = criteriaBuilder.and(predicate, previousParameter == null ? criteriaBuilder.isNull(previousPath) : criteriaBuilder.equal(previousPath, previousParameter)); - } - - orderByFields.put(path, value == null ? null : parameter); - predicates.add(predicate); - } - - return criteriaBuilder.or(toArray(predicates)); - } - - private List buildPredicates(Map criteria, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, Map parameters) { - return stream(criteria) - .map(parameter -> buildPredicate(parameter, query, criteriaBuilder, pathResolver, parameters)) - .filter(Objects::nonNull) - .collect(toList()); - } - - private Predicate buildPredicate(Entry parameter, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, Map parameters) { - var field = parameter.getKey(); - var path = pathResolver.get(elementCollections.get().contains(field) ? pathResolver.join(field) : field); - var type = ID.equals(field) ? identifierType : path.getJavaType(); - return buildTypedPredicate(path, type, field, parameter.getValue(), query, criteriaBuilder, pathResolver, new UncheckedParameterBuilder(field, criteriaBuilder, parameters)); - } - - @SuppressWarnings("unchecked") - private Predicate buildTypedPredicate(Expression path, Class type, String field, Object criteria, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, ParameterBuilder parameterBuilder) { - var alias = Alias.create(getProvider(), path, field); - var value = criteria; - var negated = value instanceof Not; - Predicate predicate; - - if (negated) { - value = ((Not) value).getValue(); - } - - try { - if (value == null || value instanceof Criteria criteriaObject && criteriaObject.getValue() == null) { - predicate = criteriaBuilder.isNull(path); - } - else if (value instanceof Criteria criteriaObject) { - predicate = criteriaObject.build(path, criteriaBuilder, parameterBuilder); - } - else if (elementCollections.get().contains(field)) { - predicate = buildElementCollectionPredicate(alias, path, type, field, value, query, criteriaBuilder, pathResolver, parameterBuilder); - } - else if (value instanceof Iterable || value.getClass().isArray()) { - predicate = buildArrayPredicate(path, type, field, value, query, criteriaBuilder, pathResolver, parameterBuilder); - } - else if (value instanceof BaseEntity) { - predicate = criteriaBuilder.equal(path, parameterBuilder.create(value)); - } - else if (type.isEnum()) { - predicate = Enumerated.parse(value, (Class>) type).build(path, criteriaBuilder, parameterBuilder); - } - else if (Numeric.is(type)) { - predicate = Numeric.parse(value, (Class) type).build(path, criteriaBuilder, parameterBuilder); - } - else if (Bool.is(type)) { - predicate = Bool.parse(value).build(path, criteriaBuilder, parameterBuilder); - } - else if (String.class.isAssignableFrom(type) || value instanceof String) { - predicate = IgnoreCase.value(value.toString()).build(path, criteriaBuilder, parameterBuilder); - } - else { - throw new UnsupportedOperationException(format(ERROR_UNSUPPORTED_CRITERIA, field, type, value, value.getClass())); - } - } - catch (IllegalArgumentException e) { - logger.log(WARNING, e, () -> format(LOG_WARNING_ILLEGAL_CRITERIA_VALUE, field, type, criteria, criteria != null ? criteria.getClass() : null)); - return null; - } - - if (negated) { - predicate = criteriaBuilder.not(predicate); - } - - alias.set(predicate); - return predicate; - } - - private Predicate buildElementCollectionPredicate(Alias alias, Expression path, Class type, String field, Object value, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, ParameterBuilder parameterBuilder) { - if (getProvider() == ECLIPSELINK || getProvider() == HIBERNATE && getDatabase() == POSTGRESQL) { - // EclipseLink refuses to perform GROUP BY on IN clause on @ElementCollection, causing a cartesian product. - // Hibernate + PostgreSQL bugs on IN clause on @ElementCollection as PostgreSQL strictly requires an additional GROUP BY, but Hibernate didn't set it. - return buildArrayPredicate(path, type, field, value, query, criteriaBuilder, pathResolver, parameterBuilder); - } - else { - // For other cases, a real IN predicate is more efficient than an array predicate, even though both approaches are supported. - return buildInPredicate(alias, path, type, value, parameterBuilder); - } - } - - private static Predicate buildInPredicate(Alias alias, Expression path, Class type, Object value, ParameterBuilder parameterBuilder) { - var in = stream(value) - .map(item -> createElementCollectionCriteria(type, item).getValue()) - .filter(Objects::nonNull) - .map(parameterBuilder::create) - .collect(toList()); - - if (in.isEmpty()) { - throw new IllegalArgumentException(value.toString()); - } - - alias.in(in.size()); - return path.in(in.toArray(new Expression[in.size()])); - } - - private Predicate buildArrayPredicate(Expression path, Class type, String field, Object value, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, ParameterBuilder parameterBuilder) { - var oneToManyField = oneToManys.test(field); - - if (oneToManyField && getProvider() == ECLIPSELINK) { - throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ONETOMANY_CRITERIA_ECLIPSELINK); // EclipseLink refuses to perform a JOIN when setFirstResult/setMaxResults is used. - } - - var elementCollectionField = elementCollections.get().contains(field); - Subquery subquery = null; - Expression fieldPath; - - if (oneToManyField || elementCollectionField) { - // This subquery must simulate an IN clause on a field of a @OneToMany or @ElementCollection relationship. - // Otherwise the main query will return ONLY the matching values while the natural expectation in UI is that they are just all returned. - subquery = query.subquery(Long.class); - Root subqueryRoot = subquery.from(entityType); - fieldPath = new RootPathResolver(subqueryRoot, elementCollections.get(), manyOrOneToOnes.get()).get(pathResolver.join(field)); - subquery.select(criteriaBuilder.countDistinct(fieldPath)).where(criteriaBuilder.equal(subqueryRoot.get(ID), pathResolver.get(ID))); - } - else { - fieldPath = path; - } - - var predicates = stream(value) - .map(item -> elementCollectionField - ? createElementCollectionCriteria(type, item).build(fieldPath, criteriaBuilder, parameterBuilder) - : buildTypedPredicate(fieldPath, type, field, item, query, criteriaBuilder, pathResolver, parameterBuilder)) - .filter(Objects::nonNull) - .collect(toList()); - - if (predicates.isEmpty()) { - throw new IllegalArgumentException(value.toString()); - } - - var predicate = criteriaBuilder.or(toArray(predicates)); - - if (subquery != null) { - // SELECT e FROM E e WHERE (SELECT COUNT(DISTINCT field) FROM T t WHERE [restrictions] AND t.id = e.id) = ACTUALCOUNT - Long actualCount = (long) predicates.size(); - predicate = criteriaBuilder.equal(subquery.where(criteriaBuilder.and(predicate, subquery.getRestriction())), actualCount); - } - - return predicate; - } - - @SuppressWarnings("unchecked") - private static Criteria createElementCollectionCriteria(Class type, Object value) { - return type.isEnum() ? Enumerated.parse(value, (Class>) type) : IgnoreCase.value(value.toString()); - } - - - // Helpers -------------------------------------------------------------------------------------------------------- - - /** - * 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 getCurrentInstance() { - try { - var ejbContext = (SessionContext) new InitialContext().lookup("java:comp/EJBContext"); - return (BaseEntityService) ejbContext.getBusinessObject(ejbContext.getInvokedBusinessInterface()); - } - catch (Exception e) { - throw new IllegalStateException(e); - } - } - - private static Predicate[] toArray(List predicates) { - return predicates.toArray(new Predicate[predicates.size()]); - } - - private static Predicate conjunctRestrictionsIfNecessary(CriteriaBuilder criteriaBuilder, Predicate nullable, Predicate nonnullable) { - return nullable == null ? nonnullable : criteriaBuilder.and(nullable, nonnullable); - } - - private static Predicate conjunctRestrictionsIfNecessary(CriteriaBuilder criteriaBuilder, Predicate nullable, List nonnullable) { - return conjunctRestrictionsIfNecessary(criteriaBuilder, nullable, criteriaBuilder.and(toArray(nonnullable))); - } - - private static Predicate buildCountPredicateIfNecessary(Predicate inPredicate, CriteriaBuilder criteriaBuilder, AbstractQuery query, PathResolver pathResolver) { - var fieldAndCount = Alias.getFieldAndCount(inPredicate); - - if (fieldAndCount.getValue() > 1) { - Expression join = pathResolver.get(pathResolver.join(fieldAndCount.getKey())); - var countPredicate = criteriaBuilder.equal(criteriaBuilder.countDistinct(join), fieldAndCount.getValue()); - Alias.setHaving(inPredicate, countPredicate); - groupByIfNecessary(query, pathResolver.get(fieldAndCount.getKey())); - return countPredicate; - } - - return null; - } - - private static void groupByIfNecessary(AbstractQuery query, Expression path) { - var groupByPath = path instanceof RootWrapper rootWrapper ? rootWrapper.getWrapped() : path; - - if (!query.getGroupList().contains(groupByPath)) { - var groupList = new ArrayList<>(query.getGroupList()); - groupList.add(groupByPath); - query.groupBy(groupList); - } - } - - private static boolean hasJoins(From from) { - return !from.getJoins().isEmpty() || hasFetches(from); - } - - private static boolean hasFetches(From from) { - return from.getFetches().stream().anyMatch(Path.class::isInstance) - || from instanceof EclipseLinkRoot eclipseLinkRoot && eclipseLinkRoot.hasPostponedFetches(); - } - - private static T noop() { - return null; - } + return buildTypedQuery(pageBuilder, countQuery, null, parameters); + } + + private Map buildCountSubquery(PageBuilder pageBuilder, CriteriaQuery countQuery, Root countRoot, CriteriaBuilder criteriaBuilder) { + var countSubquery = countQuery.subquery(pageBuilder.getResultType()); + var countSubqueryRoot = buildRoot(countSubquery); + var subqueryPathResolver = buildSelection(pageBuilder, countSubquery, countSubqueryRoot, criteriaBuilder); + var parameters = buildRestrictions(pageBuilder, countSubquery, criteriaBuilder, subqueryPathResolver); + + // SELECT COUNT(e) FROM E e WHERE EXISTS (SELECT t.id FROM T t WHERE [restrictions] AND t.id = e.id) + countQuery.where(criteriaBuilder.exists(countSubquery.where(conjunctRestrictionsIfNecessary(criteriaBuilder, countSubquery.getRestriction(), criteriaBuilder.equal(countSubqueryRoot.get(ID), countRoot.get(ID)))))); + + return parameters; + } + + private TypedQuery buildTypedQuery(PageBuilder pageBuilder, CriteriaQuery criteriaQuery, Root root, Map parameters) { + var typedQuery = getEntityManager().createQuery(criteriaQuery); + buildRange(pageBuilder, typedQuery, root); + setMappedParameters(typedQuery, parameters); + onPage(pageBuilder.getResultType(), pageBuilder.isCacheable()).accept(typedQuery); + return typedQuery; + } + + private static void setPositionalParameters(TypedQuery typedQuery, Object[] positionalParameters) { + logger.log(FINER, () -> format(LOG_FINER_SET_PARAMETER_VALUES, Arrays.toString(positionalParameters))); + range(0, positionalParameters.length).forEach(i -> typedQuery.setParameter(i, positionalParameters[i])); + } + + private static void setMappedParameters(TypedQuery typedQuery, Map mappedParameters) { + logger.log(FINER, () -> format(LOG_FINER_SET_PARAMETER_VALUES, mappedParameters)); + mappedParameters.entrySet().forEach(parameter -> typedQuery.setParameter(parameter.getKey(), parameter.getValue())); + } + + private static void setSuppliedParameters(TypedQuery typedQuery, Consumer> suppliedParameters) { + var mappedParameters = new HashMap(); + suppliedParameters.accept(mappedParameters); + setMappedParameters(typedQuery, mappedParameters); + } + + private PartialResultList executeQuery(PageBuilder pageBuilder, TypedQuery entityQuery, TypedQuery countQuery) { + var page = pageBuilder.getPage(); + var entities = entityQuery.getResultList(); + + if (pageBuilder.canBuildValueBasedPagingPredicate() && page.isReversed()) { + var reversed = new ArrayList<>(entities); + Collections.reverse(reversed); + entities = reversed; + } + + var estimatedTotalNumberOfResults = countQuery != null ? countQuery.getSingleResult().intValue() : -1; + return new PartialResultList<>(entities, page.getOffset(), estimatedTotalNumberOfResults); + } + + + // Selection actions ---------------------------------------------------------------------------------------------- + + private Root buildRoot(AbstractQuery query) { + var root = query.from(entityType); + return query instanceof Subquery ? new SubqueryRoot<>(root) : getProvider() == ECLIPSELINK ? new EclipseLinkRoot<>(root) : root; + } + + private PathResolver buildSelection(PageBuilder pageBuilder, AbstractQuery query, Root root, CriteriaBuilder criteriaBuilder) { + var mapping = pageBuilder.getQueryBuilder().build(criteriaBuilder, query, root); + + if (query instanceof Subquery subQuery) { + subQuery.select(root.get(ID)); + } + + if (!isEmpty(mapping)) { // mapping is not empty when getPage(..., MappedQueryBuilder) is used. + Map> paths = stream(mapping).collect(toMap(e -> e.getKey().getPropertyName(), Entry::getValue, (l, r) -> l, LinkedHashMap::new)); + + if (query instanceof CriteriaQuery criteriaQuery) { + criteriaQuery.multiselect(stream(paths).map(Alias::as).collect(toList())); + } + + Set aggregatedFields = paths.entrySet().stream().filter(e -> getProvider().isAggregation(e.getValue())).map(Entry::getKey).collect(toSet()); + + if (!aggregatedFields.isEmpty()) { + groupByIfNecessary(query, root); + } + + var orderingContainsAggregatedFields = aggregatedFields.removeAll(pageBuilder.getPage().getOrdering().keySet()); + pageBuilder.shouldBuildCountSubquery(true); // Normally, building of count subquery is skipped for performance, but when there's a custom mapping, we cannot reliably determine if custom criteria is used, so count subquery building cannot be reliably skipped. + pageBuilder.canBuildValueBasedPagingPredicate(getProvider() != HIBERNATE || !orderingContainsAggregatedFields); // Value based paging cannot be used in Hibernate if ordering contains aggregated fields, because Hibernate may return a cartesian product and apply firstResult/maxResults in memory. + return new MappedPathResolver(root, paths, elementCollections.get(), manyOrOneToOnes.get()); + } + else if (pageBuilder.getResultType() == entityType) { + pageBuilder.shouldBuildCountSubquery(mapping != null); // mapping is empty but not null when getPage(..., QueryBuilder) is used. + pageBuilder.canBuildValueBasedPagingPredicate(mapping == null); // when mapping is not null, we cannot reliably determine if ordering contains aggregated fields, so value based paging cannot be reliably used. + return new RootPathResolver(root, elementCollections.get(), manyOrOneToOnes.get()); + } + else { + throw new IllegalArgumentException(ERROR_ILLEGAL_MAPPING); + } + } + + private void buildRange(PageBuilder pageBuilder, Query query, Root root) { + if (root == null) { + return; + } + + var hasJoins = hasJoins(root); + var page = pageBuilder.getPage(); + + if ((hasJoins || page.getOffset() > 0) && !pageBuilder.canBuildValueBasedPagingPredicate()) { + query.setFirstResult(page.getOffset()); + } + + if (hasJoins || page.getLimit() != MAX_VALUE) { + query.setMaxResults(page.getLimit()); + } + + if (hasJoins && root instanceof EclipseLinkRoot eclipseLinkRoot) { + eclipseLinkRoot.runPostponedFetches(query); + } + } + + + // Sorting actions ------------------------------------------------------------------------------------------------ + + private void buildOrderBy(PageBuilder pageBuilder, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder, PathResolver pathResolver) { + var page = pageBuilder.getPage(); + var ordering = page.getOrdering(); + + if (ordering.isEmpty() || page.getLimit() - page.getOffset() == 1) { + return; + } + + var reversed = pageBuilder.canBuildValueBasedPagingPredicate() && page.isReversed(); + criteriaQuery.orderBy(stream(ordering).map(order -> buildOrder(order, criteriaBuilder, pathResolver, reversed)).collect(toList())); + } + + private Order buildOrder(Entry order, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, boolean reversed) { + var field = order.getKey(); + + if (oneToManys.test(field) || elementCollections.get().contains(field)) { + if (getProvider() == ECLIPSELINK) { + throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ONETOMANY_ORDERBY_ECLIPSELINK); // EclipseLink refuses to perform a JOIN when setFirstResult/setMaxResults is used. + } + else if (getProvider() == OPENJPA) { + throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ONETOMANY_ORDERBY_OPENJPA); // OpenJPA adds for some reason a second JOIN on the join table referenced in ORDER BY column causing it to be not sorted on the intended join. + } + } + + var path = pathResolver.get(field); + return order.getValue() ^ reversed ? criteriaBuilder.asc(path) : criteriaBuilder.desc(path); + } + + + // Searching actions ----------------------------------------------------------------------------------------------- + + private Map buildRestrictions(PageBuilder pageBuilder, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver) { + var page = pageBuilder.getPage(); + var parameters = new HashMap(page.getRequiredCriteria().size() + page.getOptionalCriteria().size()); + var requiredPredicates = buildPredicates(page.getRequiredCriteria(), query, criteriaBuilder, pathResolver, parameters); + var optionalPredicates = buildPredicates(page.getOptionalCriteria(), query, criteriaBuilder, pathResolver, parameters); + Predicate restriction = null; + + if (!optionalPredicates.isEmpty()) { + pageBuilder.shouldBuildCountSubquery(true); + restriction = criteriaBuilder.or(toArray(optionalPredicates)); + } + + if (!requiredPredicates.isEmpty()) { + pageBuilder.shouldBuildCountSubquery(true); + var wherePredicates = requiredPredicates.stream().filter(Alias::isWhere).collect(toList()); + + if (!wherePredicates.isEmpty()) { + restriction = conjunctRestrictionsIfNecessary(criteriaBuilder, restriction, wherePredicates); + } + + var inPredicates = wherePredicates.stream().filter(Alias::isIn).collect(toList()); + + for (var inPredicate : inPredicates) { + var countPredicate = buildCountPredicateIfNecessary(inPredicate, criteriaBuilder, query, pathResolver); + + if (countPredicate != null) { + requiredPredicates.add(countPredicate); + } + } + + var havingPredicates = requiredPredicates.stream().filter(Alias::isHaving).collect(toList()); + + if (!havingPredicates.isEmpty()) { + groupByIfNecessary(query, pathResolver.get(null)); + query.having(conjunctRestrictionsIfNecessary(criteriaBuilder, query.getGroupRestriction(), havingPredicates)); + } + } + + if (!(query instanceof Subquery) && pageBuilder.canBuildValueBasedPagingPredicate()) { + restriction = conjunctRestrictionsIfNecessary(criteriaBuilder, restriction, buildValueBasedPagingPredicate(page, criteriaBuilder, pathResolver, parameters)); + } + + if (restriction != null) { + var distinct = !optionalPredicates.isEmpty() || hasFetches((From) pathResolver.get(null)); + query.distinct(distinct).where(conjunctRestrictionsIfNecessary(criteriaBuilder, query.getRestriction(), restriction)); + } + + return parameters; + } + + @SuppressWarnings("unchecked") + private > Predicate buildValueBasedPagingPredicate(Page page, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, Map parameters) { + // Value based paging https://blog.novatec-gmbh.de/art-pagination-offset-vs-value-based-paging/ is on large offsets much faster than offset based paging. + // (orderByField1 > ?1) OR (orderByField1 = ?1 AND orderByField2 > ?2) OR (orderByField1 = ?1 AND orderByField2 = ?2 AND orderByField3 > ?3) [...] + + var predicates = new ArrayList(page.getOrdering().size()); + var orderByFields = new HashMap, ParameterExpression>(); + var last = (T) page.getLast(); + + for (var order : page.getOrdering().entrySet()) { + var field = order.getKey(); + var value = invokeGetter(last, field); + var path = (Expression) pathResolver.get(field); + ParameterExpression parameter = new UncheckedParameterBuilder(field, criteriaBuilder, parameters).create(value); + var predicate = order.getValue() ^ page.isReversed() ? criteriaBuilder.greaterThan(path, parameter) : criteriaBuilder.lessThan(path, parameter); + + for (var previousOrderByField : orderByFields.entrySet()) { + var previousPath = previousOrderByField.getKey(); + var previousParameter = previousOrderByField.getValue(); + predicate = criteriaBuilder.and(predicate, previousParameter == null ? criteriaBuilder.isNull(previousPath) : criteriaBuilder.equal(previousPath, previousParameter)); + } + + orderByFields.put(path, value == null ? null : parameter); + predicates.add(predicate); + } + + return criteriaBuilder.or(toArray(predicates)); + } + + private List buildPredicates(Map criteria, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, Map parameters) { + return stream(criteria) + .map(parameter -> buildPredicate(parameter, query, criteriaBuilder, pathResolver, parameters)) + .filter(Objects::nonNull) + .collect(toList()); + } + + private Predicate buildPredicate(Entry parameter, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, Map parameters) { + var field = parameter.getKey(); + var path = pathResolver.get(elementCollections.get().contains(field) ? pathResolver.join(field) : field); + var type = ID.equals(field) ? identifierType : path.getJavaType(); + return buildTypedPredicate(path, type, field, parameter.getValue(), query, criteriaBuilder, pathResolver, new UncheckedParameterBuilder(field, criteriaBuilder, parameters)); + } + + @SuppressWarnings("unchecked") + private Predicate buildTypedPredicate(Expression path, Class type, String field, Object criteria, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, ParameterBuilder parameterBuilder) { + var alias = Alias.create(getProvider(), path, field); + var value = criteria; + var negated = value instanceof Not; + Predicate predicate; + + if (negated) { + value = ((Not) value).getValue(); + } + + try { + if (value == null || value instanceof Criteria criteriaObject && criteriaObject.getValue() == null) { + predicate = criteriaBuilder.isNull(path); + } + else if (value instanceof Criteria criteriaObject) { + predicate = criteriaObject.build(path, criteriaBuilder, parameterBuilder); + } + else if (elementCollections.get().contains(field)) { + predicate = buildElementCollectionPredicate(alias, path, type, field, value, query, criteriaBuilder, pathResolver, parameterBuilder); + } + else if (value instanceof Iterable || value.getClass().isArray()) { + predicate = buildArrayPredicate(path, type, field, value, query, criteriaBuilder, pathResolver, parameterBuilder); + } + else if (value instanceof BaseEntity) { + predicate = criteriaBuilder.equal(path, parameterBuilder.create(value)); + } + else if (type.isEnum()) { + predicate = Enumerated.parse(value, (Class>) type).build(path, criteriaBuilder, parameterBuilder); + } + else if (Numeric.is(type)) { + predicate = Numeric.parse(value, (Class) type).build(path, criteriaBuilder, parameterBuilder); + } + else if (Bool.is(type)) { + predicate = Bool.parse(value).build(path, criteriaBuilder, parameterBuilder); + } + else if (String.class.isAssignableFrom(type) || value instanceof String) { + predicate = IgnoreCase.value(value.toString()).build(path, criteriaBuilder, parameterBuilder); + } + else { + throw new UnsupportedOperationException(format(ERROR_UNSUPPORTED_CRITERIA, field, type, value, value.getClass())); + } + } + catch (IllegalArgumentException e) { + logger.log(WARNING, e, () -> format(LOG_WARNING_ILLEGAL_CRITERIA_VALUE, field, type, criteria, criteria != null ? criteria.getClass() : null)); + return null; + } + + if (negated) { + predicate = criteriaBuilder.not(predicate); + } + + alias.set(predicate); + return predicate; + } + + private Predicate buildElementCollectionPredicate(Alias alias, Expression path, Class type, String field, Object value, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, ParameterBuilder parameterBuilder) { + if (getProvider() == ECLIPSELINK || getProvider() == HIBERNATE && getDatabase() == POSTGRESQL) { + // EclipseLink refuses to perform GROUP BY on IN clause on @ElementCollection, causing a cartesian product. + // Hibernate + PostgreSQL bugs on IN clause on @ElementCollection as PostgreSQL strictly requires an additional GROUP BY, but Hibernate didn't set it. + return buildArrayPredicate(path, type, field, value, query, criteriaBuilder, pathResolver, parameterBuilder); + } + else { + // For other cases, a real IN predicate is more efficient than an array predicate, even though both approaches are supported. + return buildInPredicate(alias, path, type, value, parameterBuilder); + } + } + + private static Predicate buildInPredicate(Alias alias, Expression path, Class type, Object value, ParameterBuilder parameterBuilder) { + var in = stream(value) + .map(item -> createElementCollectionCriteria(type, item).getValue()) + .filter(Objects::nonNull) + .map(parameterBuilder::create) + .collect(toList()); + + if (in.isEmpty()) { + throw new IllegalArgumentException(value.toString()); + } + + alias.in(in.size()); + return path.in(in.toArray(new Expression[in.size()])); + } + + private Predicate buildArrayPredicate(Expression path, Class type, String field, Object value, AbstractQuery query, CriteriaBuilder criteriaBuilder, PathResolver pathResolver, ParameterBuilder parameterBuilder) { + var oneToManyField = oneToManys.test(field); + + if (oneToManyField && getProvider() == ECLIPSELINK) { + throw new UnsupportedOperationException(ERROR_UNSUPPORTED_ONETOMANY_CRITERIA_ECLIPSELINK); // EclipseLink refuses to perform a JOIN when setFirstResult/setMaxResults is used. + } + + var elementCollectionField = elementCollections.get().contains(field); + Subquery subquery = null; + Expression fieldPath; + + if (oneToManyField || elementCollectionField) { + // This subquery must simulate an IN clause on a field of a @OneToMany or @ElementCollection relationship. + // Otherwise the main query will return ONLY the matching values while the natural expectation in UI is that they are just all returned. + subquery = query.subquery(Long.class); + Root subqueryRoot = subquery.from(entityType); + fieldPath = new RootPathResolver(subqueryRoot, elementCollections.get(), manyOrOneToOnes.get()).get(pathResolver.join(field)); + subquery.select(criteriaBuilder.countDistinct(fieldPath)).where(criteriaBuilder.equal(subqueryRoot.get(ID), pathResolver.get(ID))); + } + else { + fieldPath = path; + } + + var predicates = stream(value) + .map(item -> elementCollectionField + ? createElementCollectionCriteria(type, item).build(fieldPath, criteriaBuilder, parameterBuilder) + : buildTypedPredicate(fieldPath, type, field, item, query, criteriaBuilder, pathResolver, parameterBuilder)) + .filter(Objects::nonNull) + .collect(toList()); + + if (predicates.isEmpty()) { + throw new IllegalArgumentException(value.toString()); + } + + var predicate = criteriaBuilder.or(toArray(predicates)); + + if (subquery != null) { + // SELECT e FROM E e WHERE (SELECT COUNT(DISTINCT field) FROM T t WHERE [restrictions] AND t.id = e.id) = ACTUALCOUNT + Long actualCount = (long) predicates.size(); + predicate = criteriaBuilder.equal(subquery.where(criteriaBuilder.and(predicate, subquery.getRestriction())), actualCount); + } + + return predicate; + } + + @SuppressWarnings("unchecked") + private static Criteria createElementCollectionCriteria(Class type, Object value) { + return type.isEnum() ? Enumerated.parse(value, (Class>) type) : IgnoreCase.value(value.toString()); + } + + + // Helpers -------------------------------------------------------------------------------------------------------- + + /** + * 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 getCurrentInstance() { + try { + var ejbContext = (SessionContext) new InitialContext().lookup("java:comp/EJBContext"); + return (BaseEntityService) ejbContext.getBusinessObject(ejbContext.getInvokedBusinessInterface()); + } + catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static Predicate[] toArray(List predicates) { + return predicates.toArray(new Predicate[predicates.size()]); + } + + private static Predicate conjunctRestrictionsIfNecessary(CriteriaBuilder criteriaBuilder, Predicate nullable, Predicate nonnullable) { + return nullable == null ? nonnullable : criteriaBuilder.and(nullable, nonnullable); + } + + private static Predicate conjunctRestrictionsIfNecessary(CriteriaBuilder criteriaBuilder, Predicate nullable, List nonnullable) { + return conjunctRestrictionsIfNecessary(criteriaBuilder, nullable, criteriaBuilder.and(toArray(nonnullable))); + } + + private static Predicate buildCountPredicateIfNecessary(Predicate inPredicate, CriteriaBuilder criteriaBuilder, AbstractQuery query, PathResolver pathResolver) { + var fieldAndCount = Alias.getFieldAndCount(inPredicate); + + if (fieldAndCount.getValue() > 1) { + Expression join = pathResolver.get(pathResolver.join(fieldAndCount.getKey())); + var countPredicate = criteriaBuilder.equal(criteriaBuilder.countDistinct(join), fieldAndCount.getValue()); + Alias.setHaving(inPredicate, countPredicate); + groupByIfNecessary(query, pathResolver.get(fieldAndCount.getKey())); + return countPredicate; + } + + return null; + } + + private static void groupByIfNecessary(AbstractQuery query, Expression path) { + var groupByPath = path instanceof RootWrapper rootWrapper ? rootWrapper.getWrapped() : path; + + if (!query.getGroupList().contains(groupByPath)) { + var groupList = new ArrayList<>(query.getGroupList()); + groupList.add(groupByPath); + query.groupBy(groupList); + } + } + + private static boolean hasJoins(From from) { + return !from.getJoins().isEmpty() || hasFetches(from); + } + + private static boolean hasFetches(From from) { + return from.getFetches().stream().anyMatch(Path.class::isInstance) + || from instanceof EclipseLinkRoot eclipseLinkRoot && eclipseLinkRoot.hasPostponedFetches(); + } + + private static T noop() { + return null; + } } diff --git a/src/main/java/org/omnifaces/persistence/service/EclipseLinkRoot.java b/src/main/java/org/omnifaces/persistence/service/EclipseLinkRoot.java index d3f7442..3157216 100644 --- a/src/main/java/org/omnifaces/persistence/service/EclipseLinkRoot.java +++ b/src/main/java/org/omnifaces/persistence/service/EclipseLinkRoot.java @@ -29,37 +29,37 @@ */ class EclipseLinkRoot extends RootWrapper { - private Set postponedFetches; + private Set postponedFetches; - public EclipseLinkRoot(Root wrapped) { - super(wrapped); - postponedFetches = new HashSet<>(2); - } + public EclipseLinkRoot(Root wrapped) { + super(wrapped); + postponedFetches = new HashSet<>(2); + } - @Override - @SuppressWarnings("hiding") - public Fetch fetch(String attributeName) { - return new PostponedFetch<>(postponedFetches, attributeName); - } + @Override + @SuppressWarnings("hiding") + public Fetch fetch(String attributeName) { + return new PostponedFetch<>(postponedFetches, attributeName); + } - public boolean hasPostponedFetches() { - return !postponedFetches.isEmpty(); - } + public boolean hasPostponedFetches() { + return !postponedFetches.isEmpty(); + } - public void runPostponedFetches(Query query) { - postponedFetches.forEach(fetch -> query.setHint("eclipselink.batch", "e." + fetch)); - } + public void runPostponedFetches(Query query) { + postponedFetches.forEach(fetch -> query.setHint("eclipselink.batch", "e." + fetch)); + } - public void collectPostponedFetches(Map> paths) { - postponedFetches.forEach(fetch -> { - Path path = this; + public void collectPostponedFetches(Map> paths) { + postponedFetches.forEach(fetch -> { + Path path = this; - for (String attribute : fetch.split("\\.")) { - path = path.get(attribute); - } + for (String attribute : fetch.split("\\.")) { + path = path.get(attribute); + } - paths.put(fetch, path); - }); - } + paths.put(fetch, path); + }); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/FetchWrapper.java b/src/main/java/org/omnifaces/persistence/service/FetchWrapper.java index 51c7fc9..0ac025b 100644 --- a/src/main/java/org/omnifaces/persistence/service/FetchWrapper.java +++ b/src/main/java/org/omnifaces/persistence/service/FetchWrapper.java @@ -31,66 +31,66 @@ */ public abstract class FetchWrapper implements Fetch { - private Fetch wrapped; - - public FetchWrapper(Fetch wrapped) { - this.wrapped = wrapped; - } - - public Fetch getWrapped() { - return wrapped; - } - - @Override - public Attribute getAttribute() { - return getWrapped().getAttribute(); - } - - @Override - public Set> getFetches() { - return getWrapped().getFetches(); - } - - @Override - public FetchParent getParent() { - return getWrapped().getParent(); - } - - @Override - public JoinType getJoinType() { - return getWrapped().getJoinType(); - } - - @Override - public Fetch fetch(SingularAttribute attribute) { - return getWrapped().fetch(attribute); - } - - @Override - public Fetch fetch(SingularAttribute attribute, JoinType jt) { - return getWrapped().fetch(attribute, jt); - } - - @Override - public Fetch fetch(PluralAttribute attribute) { - return getWrapped().fetch(attribute); - } - - @Override - public Fetch fetch(PluralAttribute attribute, JoinType jt) { - return getWrapped().fetch(attribute, jt); - } - - @Override - @SuppressWarnings("hiding") - public Fetch fetch(String attributeName) { - return getWrapped().fetch(attributeName); - } - - @Override - @SuppressWarnings("hiding") - public Fetch fetch(String attributeName, JoinType jt) { - return getWrapped().fetch(attributeName, jt); - } + private Fetch wrapped; + + public FetchWrapper(Fetch wrapped) { + this.wrapped = wrapped; + } + + public Fetch getWrapped() { + return wrapped; + } + + @Override + public Attribute getAttribute() { + return getWrapped().getAttribute(); + } + + @Override + public Set> getFetches() { + return getWrapped().getFetches(); + } + + @Override + public FetchParent getParent() { + return getWrapped().getParent(); + } + + @Override + public JoinType getJoinType() { + return getWrapped().getJoinType(); + } + + @Override + public Fetch fetch(SingularAttribute attribute) { + return getWrapped().fetch(attribute); + } + + @Override + public Fetch fetch(SingularAttribute attribute, JoinType jt) { + return getWrapped().fetch(attribute, jt); + } + + @Override + public Fetch fetch(PluralAttribute attribute) { + return getWrapped().fetch(attribute); + } + + @Override + public Fetch fetch(PluralAttribute attribute, JoinType jt) { + return getWrapped().fetch(attribute, jt); + } + + @Override + @SuppressWarnings("hiding") + public Fetch fetch(String attributeName) { + return getWrapped().fetch(attributeName); + } + + @Override + @SuppressWarnings("hiding") + public Fetch fetch(String attributeName, JoinType jt) { + return getWrapped().fetch(attributeName, jt); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/JoinFetchAdapter.java b/src/main/java/org/omnifaces/persistence/service/JoinFetchAdapter.java index b9238eb..432a2f0 100644 --- a/src/main/java/org/omnifaces/persistence/service/JoinFetchAdapter.java +++ b/src/main/java/org/omnifaces/persistence/service/JoinFetchAdapter.java @@ -28,62 +28,62 @@ */ class JoinFetchAdapter implements Fetch { - private Join join; - - public JoinFetchAdapter(Join join) { - this.join = join; - } - - @Override - public Attribute getAttribute() { - return join.getAttribute(); - } - - @Override - public FetchParent getParent() { - return join.getParent(); - } - - @Override - public JoinType getJoinType() { - return join.getJoinType(); - } - - @Override - public Set> getFetches() { - return join.getFetches(); - } - - @Override - public Fetch fetch(SingularAttribute attribute) { - return new JoinFetchAdapter<>(join.join(attribute)); - } - - @Override - public Fetch fetch(SingularAttribute attribute, JoinType jt) { - return new JoinFetchAdapter<>(join.join(attribute, jt)); - } - - @Override - public Fetch fetch(PluralAttribute attribute) { - throw new UnsupportedOperationException(); - } - - @Override - public Fetch fetch(PluralAttribute attribute, JoinType jt) { - throw new UnsupportedOperationException(); - } - - @Override - @SuppressWarnings("hiding") - public Fetch fetch(String attributeName) { - return new JoinFetchAdapter<>(join.join(attributeName)); - } - - @Override - @SuppressWarnings("hiding") - public Fetch fetch(String attributeName, JoinType jt) { - return new JoinFetchAdapter<>(join.join(attributeName, jt)); - } + private Join join; + + public JoinFetchAdapter(Join join) { + this.join = join; + } + + @Override + public Attribute getAttribute() { + return join.getAttribute(); + } + + @Override + public FetchParent getParent() { + return join.getParent(); + } + + @Override + public JoinType getJoinType() { + return join.getJoinType(); + } + + @Override + public Set> getFetches() { + return join.getFetches(); + } + + @Override + public Fetch fetch(SingularAttribute attribute) { + return new JoinFetchAdapter<>(join.join(attribute)); + } + + @Override + public Fetch fetch(SingularAttribute attribute, JoinType jt) { + return new JoinFetchAdapter<>(join.join(attribute, jt)); + } + + @Override + public Fetch fetch(PluralAttribute attribute) { + throw new UnsupportedOperationException(); + } + + @Override + public Fetch fetch(PluralAttribute attribute, JoinType jt) { + throw new UnsupportedOperationException(); + } + + @Override + @SuppressWarnings("hiding") + public Fetch fetch(String attributeName) { + return new JoinFetchAdapter<>(join.join(attributeName)); + } + + @Override + @SuppressWarnings("hiding") + public Fetch fetch(String attributeName, JoinType jt) { + return new JoinFetchAdapter<>(join.join(attributeName, jt)); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/MappedPathResolver.java b/src/main/java/org/omnifaces/persistence/service/MappedPathResolver.java index 589a38b..e5653f1 100644 --- a/src/main/java/org/omnifaces/persistence/service/MappedPathResolver.java +++ b/src/main/java/org/omnifaces/persistence/service/MappedPathResolver.java @@ -23,23 +23,23 @@ */ class MappedPathResolver extends RootPathResolver { - private final Map> paths; + private final Map> paths; - public MappedPathResolver(Root root, Map> paths, Set elementCollections, Set manyOrOneToOnes) { - super(root, elementCollections, manyOrOneToOnes); - this.paths = paths; - } + public MappedPathResolver(Root root, Map> paths, Set elementCollections, Set manyOrOneToOnes) { + super(root, elementCollections, manyOrOneToOnes); + this.paths = paths; + } - @Override - public Expression get(String field) { - if (field != null) { - Expression path = paths.get(field); + @Override + public Expression get(String field) { + if (field != null) { + Expression path = paths.get(field); - if (path != null) { - return path; - } - } + if (path != null) { + return path; + } + } - return super.get(field); - } + return super.get(field); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/PageBuilder.java b/src/main/java/org/omnifaces/persistence/service/PageBuilder.java index 5a4fccd..4f03927 100644 --- a/src/main/java/org/omnifaces/persistence/service/PageBuilder.java +++ b/src/main/java/org/omnifaces/persistence/service/PageBuilder.java @@ -20,52 +20,52 @@ */ class PageBuilder { - private final Page page; - private final boolean cacheable; - private final Class resultType; - private final MappedQueryBuilder queryBuilder; + private final Page page; + private final boolean cacheable; + private final Class resultType; + private final MappedQueryBuilder queryBuilder; - private boolean shouldBuildCountSubquery; - private boolean canBuildValueBasedPagingPredicate; + private boolean shouldBuildCountSubquery; + private boolean canBuildValueBasedPagingPredicate; - public PageBuilder(Page page, boolean cacheable, Class resultType, MappedQueryBuilder queryBuilder) { - this.page = page; - this.cacheable = cacheable; - this.resultType = resultType; - this.queryBuilder = queryBuilder; - this.canBuildValueBasedPagingPredicate = page.getLast() != null && page.getOffset() > 0; - } + public PageBuilder(Page page, boolean cacheable, Class resultType, MappedQueryBuilder queryBuilder) { + this.page = page; + this.cacheable = cacheable; + this.resultType = resultType; + this.queryBuilder = queryBuilder; + this.canBuildValueBasedPagingPredicate = page.getLast() != null && page.getOffset() > 0; + } - public void shouldBuildCountSubquery(boolean yes) { - shouldBuildCountSubquery |= yes; - } + public void shouldBuildCountSubquery(boolean yes) { + shouldBuildCountSubquery |= yes; + } - public boolean shouldBuildCountSubquery() { - return shouldBuildCountSubquery; - } + public boolean shouldBuildCountSubquery() { + return shouldBuildCountSubquery; + } - public void canBuildValueBasedPagingPredicate(boolean yes) { - canBuildValueBasedPagingPredicate &= yes; - } + public void canBuildValueBasedPagingPredicate(boolean yes) { + canBuildValueBasedPagingPredicate &= yes; + } - public boolean canBuildValueBasedPagingPredicate() { - return canBuildValueBasedPagingPredicate; - } + public boolean canBuildValueBasedPagingPredicate() { + return canBuildValueBasedPagingPredicate; + } - public Page getPage() { - return page; - } + public Page getPage() { + return page; + } - public boolean isCacheable() { - return cacheable; - } + public boolean isCacheable() { + return cacheable; + } - public Class getResultType() { - return resultType; - } + public Class getResultType() { + return resultType; + } - public MappedQueryBuilder getQueryBuilder() { - return queryBuilder; - } + public MappedQueryBuilder getQueryBuilder() { + return queryBuilder; + } } diff --git a/src/main/java/org/omnifaces/persistence/service/PathResolver.java b/src/main/java/org/omnifaces/persistence/service/PathResolver.java index 9f6b66c..fdc5fb4 100644 --- a/src/main/java/org/omnifaces/persistence/service/PathResolver.java +++ b/src/main/java/org/omnifaces/persistence/service/PathResolver.java @@ -19,9 +19,9 @@ */ @FunctionalInterface interface PathResolver { - Expression get(String field); + Expression get(String field); - default String join(String field) { - return '@' + field; - } + default String join(String field) { + return '@' + field; + } } diff --git a/src/main/java/org/omnifaces/persistence/service/PostponedFetch.java b/src/main/java/org/omnifaces/persistence/service/PostponedFetch.java index f1739c9..60f056e 100644 --- a/src/main/java/org/omnifaces/persistence/service/PostponedFetch.java +++ b/src/main/java/org/omnifaces/persistence/service/PostponedFetch.java @@ -22,20 +22,20 @@ */ class PostponedFetch extends FetchWrapper { - private Set postponedFetches; - private String path; + private Set postponedFetches; + private String path; - public PostponedFetch(Set postponedFetches, String path) { - super(null); - this.postponedFetches = postponedFetches; - this.path = path; - postponedFetches.add(path); - } + public PostponedFetch(Set postponedFetches, String path) { + super(null); + this.postponedFetches = postponedFetches; + this.path = path; + postponedFetches.add(path); + } - @Override - @SuppressWarnings("hiding") - public Fetch fetch(String attributeName) { - return new PostponedFetch<>(postponedFetches, path + "." + attributeName); - } + @Override + @SuppressWarnings("hiding") + public Fetch fetch(String attributeName) { + return new PostponedFetch<>(postponedFetches, path + "." + attributeName); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/RootPathResolver.java b/src/main/java/org/omnifaces/persistence/service/RootPathResolver.java index 9af74ca..72840b6 100644 --- a/src/main/java/org/omnifaces/persistence/service/RootPathResolver.java +++ b/src/main/java/org/omnifaces/persistence/service/RootPathResolver.java @@ -31,114 +31,114 @@ */ class RootPathResolver implements PathResolver { - private static final String ERROR_UNKNOWN_FIELD = - "Field %s cannot be found on %s. If this represents a transient field, make sure that it is delegating to @ManyToOne/@OneToOne children."; - - private final Root root; - private final Map> joins; - private final Map> paths; - private final Set elementCollections; - private final Set manyOrOneToOnes; - - public RootPathResolver(Root root, Set elementCollections, Set manyOrOneToOnes) { - this.root = root; - this.joins = getJoins(root); - this.paths = new HashMap<>(); - this.elementCollections = elementCollections; - this.manyOrOneToOnes = manyOrOneToOnes; - } - - @Override - public Expression get(String field) { - if (field == null) { - return root; - } - - Path path = paths.get(field); - - if (path != null) { - return path; - } - - path = root; - boolean explicitJoin = field.charAt(0) == '@'; - String originalField = explicitJoin ? field.substring(1) : field; - String[] attributes = originalField.split("\\."); - int depth = attributes.length; - - for (int i = 0; i < depth; i++) { - String attribute = attributes[i]; - - if (i + 1 < depth || elementCollections.contains(originalField)) { - path = explicitJoin || !joins.containsKey(attribute) ? ((From) path).join(attribute) : joins.get(attribute); - } - else { - try { - path = path.get(attribute); - } - catch (IllegalArgumentException e) { - if (depth == 1 && isTransient(path.getModel().getBindableJavaType(), attribute)) { - path = guessManyOrOneToOnePath(attribute); - } - - if (depth != 1 || path == null) { - throw new IllegalArgumentException(format(ERROR_UNKNOWN_FIELD, field, root.getJavaType()), e); - } - } - } - } - - paths.put(field, path); - return path; - } - - private boolean isTransient(Class type, String property) { - return true; // TODO implement? - } - - private Path guessManyOrOneToOnePath(String attribute) { - for (String manyOrOneToOne : manyOrOneToOnes) { - try { - return (Path) get(manyOrOneToOne + "." + attribute); - } - catch (IllegalArgumentException ignore) { - continue; - } - } - - return null; - } - - private static Map> getJoins(From from) { - Map> joins = new HashMap<>(); - collectJoins(from, joins); - - if (from instanceof EclipseLinkRoot) { - ((EclipseLinkRoot) from).collectPostponedFetches(joins); - } - - return joins; - } - - private static void collectJoins(Path path, Map> joins) { - if (path instanceof From) { - ((From) path).getJoins().forEach(join -> collectJoins(join, joins)); - } - if (path instanceof FetchParent) { - try { - ((FetchParent) path).getFetches().stream().filter(fetch -> fetch instanceof Path).forEach(fetch -> collectJoins((Path) fetch, joins)); - } - catch (NullPointerException openJPAWillThrowThisOnEmptyFetches) { - // Ignore and continue. - } - } - if (path instanceof Join) { - joins.put(((Join) path).getAttribute().getName(), path); - } - else if (path instanceof Fetch) { - joins.put(((Fetch) path).getAttribute().getName(), path); - } - } + private static final String ERROR_UNKNOWN_FIELD = + "Field %s cannot be found on %s. If this represents a transient field, make sure that it is delegating to @ManyToOne/@OneToOne children."; + + private final Root root; + private final Map> joins; + private final Map> paths; + private final Set elementCollections; + private final Set manyOrOneToOnes; + + public RootPathResolver(Root root, Set elementCollections, Set manyOrOneToOnes) { + this.root = root; + this.joins = getJoins(root); + this.paths = new HashMap<>(); + this.elementCollections = elementCollections; + this.manyOrOneToOnes = manyOrOneToOnes; + } + + @Override + public Expression get(String field) { + if (field == null) { + return root; + } + + Path path = paths.get(field); + + if (path != null) { + return path; + } + + path = root; + boolean explicitJoin = field.charAt(0) == '@'; + String originalField = explicitJoin ? field.substring(1) : field; + String[] attributes = originalField.split("\\."); + int depth = attributes.length; + + for (int i = 0; i < depth; i++) { + String attribute = attributes[i]; + + if (i + 1 < depth || elementCollections.contains(originalField)) { + path = explicitJoin || !joins.containsKey(attribute) ? ((From) path).join(attribute) : joins.get(attribute); + } + else { + try { + path = path.get(attribute); + } + catch (IllegalArgumentException e) { + if (depth == 1 && isTransient(path.getModel().getBindableJavaType(), attribute)) { + path = guessManyOrOneToOnePath(attribute); + } + + if (depth != 1 || path == null) { + throw new IllegalArgumentException(format(ERROR_UNKNOWN_FIELD, field, root.getJavaType()), e); + } + } + } + } + + paths.put(field, path); + return path; + } + + private boolean isTransient(Class type, String property) { + return true; // TODO implement? + } + + private Path guessManyOrOneToOnePath(String attribute) { + for (String manyOrOneToOne : manyOrOneToOnes) { + try { + return (Path) get(manyOrOneToOne + "." + attribute); + } + catch (IllegalArgumentException ignore) { + continue; + } + } + + return null; + } + + private static Map> getJoins(From from) { + Map> joins = new HashMap<>(); + collectJoins(from, joins); + + if (from instanceof EclipseLinkRoot) { + ((EclipseLinkRoot) from).collectPostponedFetches(joins); + } + + return joins; + } + + private static void collectJoins(Path path, Map> joins) { + if (path instanceof From) { + ((From) path).getJoins().forEach(join -> collectJoins(join, joins)); + } + if (path instanceof FetchParent) { + try { + ((FetchParent) path).getFetches().stream().filter(fetch -> fetch instanceof Path).forEach(fetch -> collectJoins((Path) fetch, joins)); + } + catch (NullPointerException openJPAWillThrowThisOnEmptyFetches) { + // Ignore and continue. + } + } + if (path instanceof Join) { + joins.put(((Join) path).getAttribute().getName(), path); + } + else if (path instanceof Fetch) { + joins.put(((Fetch) path).getAttribute().getName(), path); + } + } } diff --git a/src/main/java/org/omnifaces/persistence/service/RootWrapper.java b/src/main/java/org/omnifaces/persistence/service/RootWrapper.java index 5dffc03..f112de2 100644 --- a/src/main/java/org/omnifaces/persistence/service/RootWrapper.java +++ b/src/main/java/org/omnifaces/persistence/service/RootWrapper.java @@ -47,272 +47,272 @@ */ public abstract class RootWrapper implements Root { - private Root wrapped; - - public RootWrapper(Root wrapped) { - this.wrapped = wrapped; - } - - public Root getWrapped() { - return wrapped; - } - - @Override - public Predicate isNull() { - return getWrapped().isNull(); - } - - @Override - public Class getJavaType() { - return getWrapped().getJavaType(); - } - - @Override - public EntityType getModel() { - return getWrapped().getModel(); - } - - @Override - public Selection alias(String name) { - return getWrapped().alias(name); - } - - @Override - public Set> getFetches() { - return getWrapped().getFetches(); - } - - @Override - public Predicate isNotNull() { - return getWrapped().isNotNull(); - } - - @Override - public String getAlias() { - return getWrapped().getAlias(); - } - - @Override - public Predicate in(Object... values) { - return getWrapped().in(values); - } - - @Override - public boolean isCompoundSelection() { - return getWrapped().isCompoundSelection(); - } - - @Override - public Path getParentPath() { - return getWrapped().getParentPath(); - } - - @Override - public Fetch fetch(SingularAttribute attribute) { - return getWrapped().fetch(attribute); - } - - @Override - public List> getCompoundSelectionItems() { - return getWrapped().getCompoundSelectionItems(); - } - - @Override - public Path get(SingularAttribute attribute) { - return getWrapped().get(attribute); - } - - @Override - public Set> getJoins() { - return getWrapped().getJoins(); - } - - @Override - public Predicate in(Expression... values) { - return getWrapped().in(values); - } - - @Override - public Fetch fetch(SingularAttribute attribute, JoinType jt) { - return getWrapped().fetch(attribute, jt); - } - - @Override - public > Expression get(PluralAttribute collection) { - return getWrapped().get(collection); - } - - @Override - public Predicate in(Collection values) { - return getWrapped().in(values); - } - - @Override - public boolean isCorrelated() { - return getWrapped().isCorrelated(); - } - - @Override - public Fetch fetch(PluralAttribute attribute) { - return getWrapped().fetch(attribute); - } - - @Override - public Predicate in(Expression> values) { - return getWrapped().in(values); - } - - @Override - public > Expression get(MapAttribute map) { - return getWrapped().get(map); - } - - @Override - public From getCorrelationParent() { - return getWrapped().getCorrelationParent(); - } - - @Override - public Fetch fetch(PluralAttribute attribute, JoinType jt) { - return getWrapped().fetch(attribute, jt); - } - - @Override - @SuppressWarnings("hiding") - public Expression as(Class type) { - return getWrapped().as(type); - } - - @Override - public Expression> type() { - return getWrapped().type(); - } - - @Override - @SuppressWarnings("hiding") - public Fetch fetch(String attributeName) { - return getWrapped().fetch(attributeName); - } - - @Override - public Join join(SingularAttribute attribute) { - return getWrapped().join(attribute); - } - - @Override - public Path get(String attributeName) { - return getWrapped().get(attributeName); - } - - @Override - public Join join(SingularAttribute attribute, JoinType jt) { - return getWrapped().join(attribute, jt); - } - - @Override - @SuppressWarnings("hiding") - public Fetch fetch(String attributeName, JoinType jt) { - return getWrapped().fetch(attributeName, jt); - } - - @Override - public CollectionJoin join(CollectionAttribute collection) { - return getWrapped().join(collection); - } - - @Override - public SetJoin join(SetAttribute set) { - return getWrapped().join(set); - } - - @Override - public ListJoin join(ListAttribute list) { - return getWrapped().join(list); - } - - @Override - public MapJoin join(MapAttribute map) { - return getWrapped().join(map); - } - - @Override - public CollectionJoin join(CollectionAttribute collection, JoinType jt) { - return getWrapped().join(collection, jt); - } - - @Override - public SetJoin join(SetAttribute set, JoinType jt) { - return getWrapped().join(set, jt); - } - - @Override - public ListJoin join(ListAttribute list, JoinType jt) { - return getWrapped().join(list, jt); - } - - @Override - public MapJoin join(MapAttribute map, JoinType jt) { - return getWrapped().join(map, jt); - } - - @Override - @SuppressWarnings("hiding") - public Join join(String attributeName) { - return getWrapped().join(attributeName); - } - - @Override - @SuppressWarnings("hiding") - public CollectionJoin joinCollection(String attributeName) { - return getWrapped().joinCollection(attributeName); - } - - @Override - @SuppressWarnings("hiding") - public SetJoin joinSet(String attributeName) { - return getWrapped().joinSet(attributeName); - } - - @Override - @SuppressWarnings("hiding") - public ListJoin joinList(String attributeName) { - return getWrapped().joinList(attributeName); - } - - @Override - @SuppressWarnings("hiding") - public MapJoin joinMap(String attributeName) { - return getWrapped().joinMap(attributeName); - } - - @Override - @SuppressWarnings("hiding") - public Join join(String attributeName, JoinType jt) { - return getWrapped().join(attributeName, jt); - } - - @Override - @SuppressWarnings("hiding") - public CollectionJoin joinCollection(String attributeName, JoinType jt) { - return getWrapped().joinCollection(attributeName, jt); - } - - @Override - @SuppressWarnings("hiding") - public SetJoin joinSet(String attributeName, JoinType jt) { - return getWrapped().joinSet(attributeName, jt); - } - - @Override - @SuppressWarnings("hiding") - public ListJoin joinList(String attributeName, JoinType jt) { - return getWrapped().joinList(attributeName, jt); - } - - @Override - @SuppressWarnings("hiding") - public MapJoin joinMap(String attributeName, JoinType jt) { - return getWrapped().joinMap(attributeName, jt); - } + private Root wrapped; + + public RootWrapper(Root wrapped) { + this.wrapped = wrapped; + } + + public Root getWrapped() { + return wrapped; + } + + @Override + public Predicate isNull() { + return getWrapped().isNull(); + } + + @Override + public Class getJavaType() { + return getWrapped().getJavaType(); + } + + @Override + public EntityType getModel() { + return getWrapped().getModel(); + } + + @Override + public Selection alias(String name) { + return getWrapped().alias(name); + } + + @Override + public Set> getFetches() { + return getWrapped().getFetches(); + } + + @Override + public Predicate isNotNull() { + return getWrapped().isNotNull(); + } + + @Override + public String getAlias() { + return getWrapped().getAlias(); + } + + @Override + public Predicate in(Object... values) { + return getWrapped().in(values); + } + + @Override + public boolean isCompoundSelection() { + return getWrapped().isCompoundSelection(); + } + + @Override + public Path getParentPath() { + return getWrapped().getParentPath(); + } + + @Override + public Fetch fetch(SingularAttribute attribute) { + return getWrapped().fetch(attribute); + } + + @Override + public List> getCompoundSelectionItems() { + return getWrapped().getCompoundSelectionItems(); + } + + @Override + public Path get(SingularAttribute attribute) { + return getWrapped().get(attribute); + } + + @Override + public Set> getJoins() { + return getWrapped().getJoins(); + } + + @Override + public Predicate in(Expression... values) { + return getWrapped().in(values); + } + + @Override + public Fetch fetch(SingularAttribute attribute, JoinType jt) { + return getWrapped().fetch(attribute, jt); + } + + @Override + public > Expression get(PluralAttribute collection) { + return getWrapped().get(collection); + } + + @Override + public Predicate in(Collection values) { + return getWrapped().in(values); + } + + @Override + public boolean isCorrelated() { + return getWrapped().isCorrelated(); + } + + @Override + public Fetch fetch(PluralAttribute attribute) { + return getWrapped().fetch(attribute); + } + + @Override + public Predicate in(Expression> values) { + return getWrapped().in(values); + } + + @Override + public > Expression get(MapAttribute map) { + return getWrapped().get(map); + } + + @Override + public From getCorrelationParent() { + return getWrapped().getCorrelationParent(); + } + + @Override + public Fetch fetch(PluralAttribute attribute, JoinType jt) { + return getWrapped().fetch(attribute, jt); + } + + @Override + @SuppressWarnings("hiding") + public Expression as(Class type) { + return getWrapped().as(type); + } + + @Override + public Expression> type() { + return getWrapped().type(); + } + + @Override + @SuppressWarnings("hiding") + public Fetch fetch(String attributeName) { + return getWrapped().fetch(attributeName); + } + + @Override + public Join join(SingularAttribute attribute) { + return getWrapped().join(attribute); + } + + @Override + public Path get(String attributeName) { + return getWrapped().get(attributeName); + } + + @Override + public Join join(SingularAttribute attribute, JoinType jt) { + return getWrapped().join(attribute, jt); + } + + @Override + @SuppressWarnings("hiding") + public Fetch fetch(String attributeName, JoinType jt) { + return getWrapped().fetch(attributeName, jt); + } + + @Override + public CollectionJoin join(CollectionAttribute collection) { + return getWrapped().join(collection); + } + + @Override + public SetJoin join(SetAttribute set) { + return getWrapped().join(set); + } + + @Override + public ListJoin join(ListAttribute list) { + return getWrapped().join(list); + } + + @Override + public MapJoin join(MapAttribute map) { + return getWrapped().join(map); + } + + @Override + public CollectionJoin join(CollectionAttribute collection, JoinType jt) { + return getWrapped().join(collection, jt); + } + + @Override + public SetJoin join(SetAttribute set, JoinType jt) { + return getWrapped().join(set, jt); + } + + @Override + public ListJoin join(ListAttribute list, JoinType jt) { + return getWrapped().join(list, jt); + } + + @Override + public MapJoin join(MapAttribute map, JoinType jt) { + return getWrapped().join(map, jt); + } + + @Override + @SuppressWarnings("hiding") + public Join join(String attributeName) { + return getWrapped().join(attributeName); + } + + @Override + @SuppressWarnings("hiding") + public CollectionJoin joinCollection(String attributeName) { + return getWrapped().joinCollection(attributeName); + } + + @Override + @SuppressWarnings("hiding") + public SetJoin joinSet(String attributeName) { + return getWrapped().joinSet(attributeName); + } + + @Override + @SuppressWarnings("hiding") + public ListJoin joinList(String attributeName) { + return getWrapped().joinList(attributeName); + } + + @Override + @SuppressWarnings("hiding") + public MapJoin joinMap(String attributeName) { + return getWrapped().joinMap(attributeName); + } + + @Override + @SuppressWarnings("hiding") + public Join join(String attributeName, JoinType jt) { + return getWrapped().join(attributeName, jt); + } + + @Override + @SuppressWarnings("hiding") + public CollectionJoin joinCollection(String attributeName, JoinType jt) { + return getWrapped().joinCollection(attributeName, jt); + } + + @Override + @SuppressWarnings("hiding") + public SetJoin joinSet(String attributeName, JoinType jt) { + return getWrapped().joinSet(attributeName, jt); + } + + @Override + @SuppressWarnings("hiding") + public ListJoin joinList(String attributeName, JoinType jt) { + return getWrapped().joinList(attributeName, jt); + } + + @Override + @SuppressWarnings("hiding") + public MapJoin joinMap(String attributeName, JoinType jt) { + return getWrapped().joinMap(attributeName, jt); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/SoftDeleteData.java b/src/main/java/org/omnifaces/persistence/service/SoftDeleteData.java index 6bf2963..9f6bebd 100644 --- a/src/main/java/org/omnifaces/persistence/service/SoftDeleteData.java +++ b/src/main/java/org/omnifaces/persistence/service/SoftDeleteData.java @@ -30,69 +30,69 @@ */ class SoftDeleteData { - private static final String ERROR_NOT_SOFT_DELETABLE = - "Entity %s cannot be soft deleted. You need to add a @SoftDeletable field first."; - private static final String ERROR_ILLEGAL_SOFT_DELETABLE = - "Entity %s cannot be soft deleted. There should be only one @SoftDeletable field."; - - private Class entityType; - private final boolean softDeletable; - private final String fieldName; - private final String setterName; - private final boolean typeActive; - - public SoftDeleteData(Class entityType) { - this.entityType = entityType; - List softDeletableFields = listAnnotatedFields(entityType, SoftDeletable.class); - - if (softDeletableFields.isEmpty()) { - this.softDeletable = false; - this.fieldName = null; - this.setterName = null; - this.typeActive = false; - } - else if (softDeletableFields.size() == 1) { - Field softDeletableField = softDeletableFields.get(0); - this.softDeletable = true; - this.fieldName = softDeletableField.getName(); - this.setterName = ("set" + toUpperCase(fieldName.charAt(0)) + fieldName.substring(1)); - this.typeActive = softDeletableField.getAnnotation(SoftDeletable.class).type() == SoftDeletable.Type.ACTIVE; - } - else { - throw new IllegalStateException(format(ERROR_ILLEGAL_SOFT_DELETABLE, entityType)); - } - } - - public void checkSoftDeletable() { - if (!softDeletable) { - throw new NonSoftDeletableEntityException(null, format(ERROR_NOT_SOFT_DELETABLE, entityType)); - } - } - - public boolean isSoftDeleted(BaseEntity entity) { - if (!softDeletable) { - return false; - } - - boolean value = accessField(entity, fieldName); - return typeActive ? !value : value; - } - - public void setSoftDeleted(BaseEntity entity, boolean deleted) { - invokeMethod(entity, setterName, typeActive ? !deleted : deleted); - } - - public String getWhereClause(boolean includeSoftDeleted) { - if (!softDeletable) { - return ""; - } - - return (" WHERE e." + fieldName + (includeSoftDeleted ? "=" : "!=") + (typeActive ? "false": "true")); - } - - @Override - public String toString() { - return format("SoftDeleteData[softDeletable=%s, fieldName=%s, setterName=%s, typeActive=%s]", softDeletable, fieldName, setterName, typeActive); - } + private static final String ERROR_NOT_SOFT_DELETABLE = + "Entity %s cannot be soft deleted. You need to add a @SoftDeletable field first."; + private static final String ERROR_ILLEGAL_SOFT_DELETABLE = + "Entity %s cannot be soft deleted. There should be only one @SoftDeletable field."; + + private Class entityType; + private final boolean softDeletable; + private final String fieldName; + private final String setterName; + private final boolean typeActive; + + public SoftDeleteData(Class entityType) { + this.entityType = entityType; + List softDeletableFields = listAnnotatedFields(entityType, SoftDeletable.class); + + if (softDeletableFields.isEmpty()) { + this.softDeletable = false; + this.fieldName = null; + this.setterName = null; + this.typeActive = false; + } + else if (softDeletableFields.size() == 1) { + Field softDeletableField = softDeletableFields.get(0); + this.softDeletable = true; + this.fieldName = softDeletableField.getName(); + this.setterName = ("set" + toUpperCase(fieldName.charAt(0)) + fieldName.substring(1)); + this.typeActive = softDeletableField.getAnnotation(SoftDeletable.class).type() == SoftDeletable.Type.ACTIVE; + } + else { + throw new IllegalStateException(format(ERROR_ILLEGAL_SOFT_DELETABLE, entityType)); + } + } + + public void checkSoftDeletable() { + if (!softDeletable) { + throw new NonSoftDeletableEntityException(null, format(ERROR_NOT_SOFT_DELETABLE, entityType)); + } + } + + public boolean isSoftDeleted(BaseEntity entity) { + if (!softDeletable) { + return false; + } + + boolean value = accessField(entity, fieldName); + return typeActive ? !value : value; + } + + public void setSoftDeleted(BaseEntity entity, boolean deleted) { + invokeMethod(entity, setterName, typeActive ? !deleted : deleted); + } + + public String getWhereClause(boolean includeSoftDeleted) { + if (!softDeletable) { + return ""; + } + + return (" WHERE e." + fieldName + (includeSoftDeleted ? "=" : "!=") + (typeActive ? "false": "true")); + } + + @Override + public String toString() { + return format("SoftDeleteData[softDeletable=%s, fieldName=%s, setterName=%s, typeActive=%s]", softDeletable, fieldName, setterName, typeActive); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/SubqueryRoot.java b/src/main/java/org/omnifaces/persistence/service/SubqueryRoot.java index be1c60f..f908557 100644 --- a/src/main/java/org/omnifaces/persistence/service/SubqueryRoot.java +++ b/src/main/java/org/omnifaces/persistence/service/SubqueryRoot.java @@ -30,77 +30,77 @@ */ class SubqueryRoot extends RootWrapper { - public SubqueryRoot(Root wrapped) { - super(wrapped); - } + public SubqueryRoot(Root wrapped) { + super(wrapped); + } - @Override - @SuppressWarnings({ "hiding" }) - public Fetch fetch(String attributeName) { - return new JoinFetchAdapter<>(join(attributeName)); - } + @Override + @SuppressWarnings({ "hiding" }) + public Fetch fetch(String attributeName) { + return new JoinFetchAdapter<>(join(attributeName)); + } - @Override - @SuppressWarnings({ "hiding" }) - public Fetch fetch(String attributeName, JoinType joinType) { - return new JoinFetchAdapter<>(join(attributeName, joinType)); - } + @Override + @SuppressWarnings({ "hiding" }) + public Fetch fetch(String attributeName, JoinType joinType) { + return new JoinFetchAdapter<>(join(attributeName, joinType)); + } - @Override - public Fetch fetch(SingularAttribute attribute) { - return new JoinFetchAdapter<>(join(attribute)); - } + @Override + public Fetch fetch(SingularAttribute attribute) { + return new JoinFetchAdapter<>(join(attribute)); + } - @Override - public Fetch fetch(SingularAttribute attribute, JoinType joinType) { - return new JoinFetchAdapter<>(join(attribute, joinType)); - } + @Override + public Fetch fetch(SingularAttribute attribute, JoinType joinType) { + return new JoinFetchAdapter<>(join(attribute, joinType)); + } - @Override - @SuppressWarnings("unchecked") - public Fetch fetch(PluralAttribute attribute) { - Join join; + @Override + @SuppressWarnings("unchecked") + public Fetch fetch(PluralAttribute attribute) { + Join join; - if (attribute instanceof ListAttribute) { - join = join((ListAttribute) attribute); - } - else if (attribute instanceof SetAttribute) { - join = join((SetAttribute) attribute); - } - else if (attribute instanceof MapAttribute) { - join = join((MapAttribute) attribute); - } - else if (attribute instanceof CollectionAttribute) { - join = join((CollectionAttribute) attribute); - } - else { - throw new UnsupportedOperationException(); - } + if (attribute instanceof ListAttribute) { + join = join((ListAttribute) attribute); + } + else if (attribute instanceof SetAttribute) { + join = join((SetAttribute) attribute); + } + else if (attribute instanceof MapAttribute) { + join = join((MapAttribute) attribute); + } + else if (attribute instanceof CollectionAttribute) { + join = join((CollectionAttribute) attribute); + } + else { + throw new UnsupportedOperationException(); + } - return new JoinFetchAdapter<>(join); - } + return new JoinFetchAdapter<>(join); + } - @Override - @SuppressWarnings("unchecked") - public Fetch fetch(PluralAttribute attribute, JoinType joinType) { - Join join; + @Override + @SuppressWarnings("unchecked") + public Fetch fetch(PluralAttribute attribute, JoinType joinType) { + Join join; - if (attribute instanceof ListAttribute) { - join = join((ListAttribute) attribute, joinType); - } - else if (attribute instanceof SetAttribute) { - join = join((SetAttribute) attribute, joinType); - } - else if (attribute instanceof MapAttribute) { - join = join((MapAttribute) attribute, joinType); - } - else if (attribute instanceof CollectionAttribute) { - join = join((CollectionAttribute) attribute, joinType); - } - else { - throw new UnsupportedOperationException(); - } + if (attribute instanceof ListAttribute) { + join = join((ListAttribute) attribute, joinType); + } + else if (attribute instanceof SetAttribute) { + join = join((SetAttribute) attribute, joinType); + } + else if (attribute instanceof MapAttribute) { + join = join((MapAttribute) attribute, joinType); + } + else if (attribute instanceof CollectionAttribute) { + join = join((CollectionAttribute) attribute, joinType); + } + else { + throw new UnsupportedOperationException(); + } - return new JoinFetchAdapter<>(join); - } + return new JoinFetchAdapter<>(join); + } } diff --git a/src/main/java/org/omnifaces/persistence/service/UncheckedParameterBuilder.java b/src/main/java/org/omnifaces/persistence/service/UncheckedParameterBuilder.java index 25d2ca8..7613f5c 100644 --- a/src/main/java/org/omnifaces/persistence/service/UncheckedParameterBuilder.java +++ b/src/main/java/org/omnifaces/persistence/service/UncheckedParameterBuilder.java @@ -24,24 +24,24 @@ */ class UncheckedParameterBuilder implements ParameterBuilder { - private final String field; - private final CriteriaBuilder criteriaBuilder; - private final Map parameters; - - public UncheckedParameterBuilder(String field, CriteriaBuilder criteriaBuilder, Map parameters) { - this.field = field.replace('.', '$') + "_"; - this.criteriaBuilder = criteriaBuilder; - this.parameters = parameters; - } - - @Override - @SuppressWarnings("unchecked") - public ParameterExpression create(Object value) { - String name = field + parameters.size(); - parameters.put(name, value); - Class type = (value == null) ? Object.class : value.getClass(); - return (ParameterExpression) criteriaBuilder.parameter(type, name); - } + private final String field; + private final CriteriaBuilder criteriaBuilder; + private final Map parameters; + + public UncheckedParameterBuilder(String field, CriteriaBuilder criteriaBuilder, Map parameters) { + this.field = field.replace('.', '$') + "_"; + this.criteriaBuilder = criteriaBuilder; + this.parameters = parameters; + } + + @Override + @SuppressWarnings("unchecked") + public ParameterExpression create(Object value) { + String name = field + parameters.size(); + parameters.put(name, value); + Class type = (value == null) ? Object.class : value.getClass(); + return (ParameterExpression) criteriaBuilder.parameter(type, name); + } } diff --git a/src/main/resources/META-INF/beans.xml b/src/main/resources/META-INF/beans.xml index b47e0c9..15f7066 100644 --- a/src/main/resources/META-INF/beans.xml +++ b/src/main/resources/META-INF/beans.xml @@ -14,9 +14,9 @@ --> \ No newline at end of file diff --git a/src/test/java/org/omnifaces/persistence/test/OmniPersistenceTest.java b/src/test/java/org/omnifaces/persistence/test/OmniPersistenceTest.java index f38fba4..595710b 100644 --- a/src/test/java/org/omnifaces/persistence/test/OmniPersistenceTest.java +++ b/src/test/java/org/omnifaces/persistence/test/OmniPersistenceTest.java @@ -54,21 +54,21 @@ @ExtendWith(ArquillianExtension.class) public class OmniPersistenceTest { - @Deployment - public static WebArchive createDeployment() { - var maven = Maven.resolver(); - return create(WebArchive.class) - .addPackages(true, OmniPersistenceTest.class.getPackage()) - .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml") - .addAsWebInfResource("web.xml") - .addAsResource("META-INF/persistence.xml") - .addAsResource("META-INF/sql/create-test.sql") - .addAsResource("META-INF/sql/drop-test.sql") - .addAsResource("META-INF/sql/load-test.sql") - .addAsLibrary(create(MavenImporter.class).loadPomFromFile("pom.xml").importBuildOutput().as(JavaArchive.class)) - .addAsLibraries(maven.loadPomFromFile("pom.xml").importCompileAndRuntimeDependencies().resolve().withTransitivity().asFile()) - .addAsLibraries(maven.resolve("com.h2database:h2:" + getProperty("test.h2.version")).withTransitivity().asFile()); - } + @Deployment + public static WebArchive createDeployment() { + var maven = Maven.resolver(); + return create(WebArchive.class) + .addPackages(true, OmniPersistenceTest.class.getPackage()) + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml") + .addAsWebInfResource("web.xml") + .addAsResource("META-INF/persistence.xml") + .addAsResource("META-INF/sql/create-test.sql") + .addAsResource("META-INF/sql/drop-test.sql") + .addAsResource("META-INF/sql/load-test.sql") + .addAsLibrary(create(MavenImporter.class).loadPomFromFile("pom.xml").importBuildOutput().as(JavaArchive.class)) + .addAsLibraries(maven.loadPomFromFile("pom.xml").importCompileAndRuntimeDependencies().resolve().withTransitivity().asFile()) + .addAsLibraries(maven.resolve("com.h2database:h2:" + getProperty("test.h2.version")).withTransitivity().asFile()); + } @EJB private PersonService personService; @@ -76,114 +76,114 @@ public static WebArchive createDeployment() { @EJB private PhoneService phoneService; - @EJB - private TextService textService; - - @EJB - private CommentService commentService; - - @EJB - private LookupService lookupService; - - protected static boolean isEclipseLink() { - return getenv("MAVEN_CMD_LINE_ARGS").endsWith("-eclipselink"); - } - - // Basic ---------------------------------------------------------------------------------------------------------- - - @Test - void testFindPerson() { - var existingPerson = personService.findById(1L); - assertTrue(existingPerson.isPresent(), "Existing person"); - var nonExistingPerson = personService.findById(0L); - assertFalse(nonExistingPerson.isPresent(), "Non-existing person"); - } - - @Test - void testGetPerson() { - var existingPerson = personService.getById(1L); - assertNotNull(existingPerson, "Existing person"); - var nonExistingPerson = personService.getById(0L); - assertNull(nonExistingPerson, "Non-existing person"); - } - - @Test - void testPersistAndDeleteNewPerson() { - var newPerson = createNewPerson("testPersistNewPerson@example.com"); - personService.persist(newPerson); - Long expectedNewId = TOTAL_RECORDS + 1L; - assertEquals(expectedNewId, newPerson.getId(), "New person ID"); - assertEquals(TOTAL_RECORDS + 1, personService.list().size(), "Total records"); - - personService.delete(newPerson); - assertEquals(TOTAL_RECORDS, personService.list().size(), "Total records"); - } - - @Test - void testPersistExistingPerson() { - var existingPerson = createNewPerson("testPersistExistingPerson@example.com"); - existingPerson.setId(1L); - assertThrows(IllegalEntityStateException.class, () -> personService.persist(existingPerson)); - } - - @Test - void testUpdateExistingPerson() { - var existingPerson = personService.getById(1L); - assertNotNull(existingPerson, "Existing person"); - var newEmail = "testUpdateExistingPerson@example.com"; - existingPerson.setEmail(newEmail); - personService.update(existingPerson); - var existingPersonAfterUpdate = personService.getById(1L); - assertEquals(newEmail, existingPersonAfterUpdate.getEmail(), "Email updated"); - } - - @Test - void testUpdateNewPerson() { - var newPerson = createNewPerson("testUpdateNewPerson@example.com"); - assertThrows(IllegalEntityStateException.class, () -> personService.update(newPerson)); - } - - @Test - void testResetExistingPerson() { - var existingPerson = personService.getById(1L); - assertNotNull(existingPerson, "Existing person"); - var oldEmail = existingPerson.getEmail(); - existingPerson.setEmail("testResetExistingPerson@example.com"); - personService.reset(existingPerson); - assertEquals(oldEmail, existingPerson.getEmail(), "Email resetted"); - } - - @Test - void testResetNonExistingPerson() { - var nonExistingPerson = createNewPerson("testResetNonExistingPerson@example.com"); - assertThrows(IllegalEntityStateException.class, () -> personService.reset(nonExistingPerson)); - } - - @Test - void testDeleteNonExistingPerson() { - var nonExistingPerson = createNewPerson("testDeleteNonExistingPerson@example.com"); - assertThrows(IllegalEntityStateException.class, () -> personService.delete(nonExistingPerson)); - } - - private static Person createNewPerson(String email) { - var person = new Person(); - person.setEmail(email); - person.setGender(Gender.OTHER); - person.setDateOfBirth(LocalDate.now()); - return person; - } - - - // Page ----------------------------------------------------------------------------------------------------------- - - @Test - void testPage() { - var persons = personService.getPage(Page.ALL, true); - assertEquals(TOTAL_RECORDS, persons.size(), "There are 200 records"); - - var males = personService.getPage(Page.with().anyMatch(Collections.singletonMap("gender", Gender.MALE)).build(), true); - assertTrue(males.size() < TOTAL_RECORDS, "There are less than 200 records"); - } + @EJB + private TextService textService; + + @EJB + private CommentService commentService; + + @EJB + private LookupService lookupService; + + protected static boolean isEclipseLink() { + return getenv("MAVEN_CMD_LINE_ARGS").endsWith("-eclipselink"); + } + + // Basic ---------------------------------------------------------------------------------------------------------- + + @Test + void testFindPerson() { + var existingPerson = personService.findById(1L); + assertTrue(existingPerson.isPresent(), "Existing person"); + var nonExistingPerson = personService.findById(0L); + assertFalse(nonExistingPerson.isPresent(), "Non-existing person"); + } + + @Test + void testGetPerson() { + var existingPerson = personService.getById(1L); + assertNotNull(existingPerson, "Existing person"); + var nonExistingPerson = personService.getById(0L); + assertNull(nonExistingPerson, "Non-existing person"); + } + + @Test + void testPersistAndDeleteNewPerson() { + var newPerson = createNewPerson("testPersistNewPerson@example.com"); + personService.persist(newPerson); + Long expectedNewId = TOTAL_RECORDS + 1L; + assertEquals(expectedNewId, newPerson.getId(), "New person ID"); + assertEquals(TOTAL_RECORDS + 1, personService.list().size(), "Total records"); + + personService.delete(newPerson); + assertEquals(TOTAL_RECORDS, personService.list().size(), "Total records"); + } + + @Test + void testPersistExistingPerson() { + var existingPerson = createNewPerson("testPersistExistingPerson@example.com"); + existingPerson.setId(1L); + assertThrows(IllegalEntityStateException.class, () -> personService.persist(existingPerson)); + } + + @Test + void testUpdateExistingPerson() { + var existingPerson = personService.getById(1L); + assertNotNull(existingPerson, "Existing person"); + var newEmail = "testUpdateExistingPerson@example.com"; + existingPerson.setEmail(newEmail); + personService.update(existingPerson); + var existingPersonAfterUpdate = personService.getById(1L); + assertEquals(newEmail, existingPersonAfterUpdate.getEmail(), "Email updated"); + } + + @Test + void testUpdateNewPerson() { + var newPerson = createNewPerson("testUpdateNewPerson@example.com"); + assertThrows(IllegalEntityStateException.class, () -> personService.update(newPerson)); + } + + @Test + void testResetExistingPerson() { + var existingPerson = personService.getById(1L); + assertNotNull(existingPerson, "Existing person"); + var oldEmail = existingPerson.getEmail(); + existingPerson.setEmail("testResetExistingPerson@example.com"); + personService.reset(existingPerson); + assertEquals(oldEmail, existingPerson.getEmail(), "Email resetted"); + } + + @Test + void testResetNonExistingPerson() { + var nonExistingPerson = createNewPerson("testResetNonExistingPerson@example.com"); + assertThrows(IllegalEntityStateException.class, () -> personService.reset(nonExistingPerson)); + } + + @Test + void testDeleteNonExistingPerson() { + var nonExistingPerson = createNewPerson("testDeleteNonExistingPerson@example.com"); + assertThrows(IllegalEntityStateException.class, () -> personService.delete(nonExistingPerson)); + } + + private static Person createNewPerson(String email) { + var person = new Person(); + person.setEmail(email); + person.setGender(Gender.OTHER); + person.setDateOfBirth(LocalDate.now()); + return person; + } + + + // Page ----------------------------------------------------------------------------------------------------------- + + @Test + void testPage() { + var persons = personService.getPage(Page.ALL, true); + assertEquals(TOTAL_RECORDS, persons.size(), "There are 200 records"); + + var males = personService.getPage(Page.with().anyMatch(Collections.singletonMap("gender", Gender.MALE)).build(), true); + assertTrue(males.size() < TOTAL_RECORDS, "There are less than 200 records"); + } @Test void testPageByLazyManyToOne() { // This was failing since Hibernate 6 upgrade. @@ -192,126 +192,126 @@ void testPageByLazyManyToOne() { // This was failing since Hibernate 6 upgrade. assertEquals(TOTAL_PHONES_PER_PERSON_0, phones.size(), "There are 3 phones"); } - // @SoftDeletable ------------------------------------------------------------------------------------------------- - - @Test - void testSoftDelete() { - var allTexts = textService.list(); - var allComments = commentService.list(); - - var activeText = textService.getById(1L); - textService.softDelete(activeText); - var activeTextAfterSoftDelete = textService.getSoftDeletedById(1L); - assertFalse(activeTextAfterSoftDelete.isActive(), "Text entity was soft deleted"); - assertEquals(allTexts.size() - 1, textService.list().size(), "Total records for texts"); - assertEquals(1, textService.listSoftDeleted().size(), "Total deleted records for texts"); - - var activeComment = commentService.getById(1L); - commentService.softDelete(activeComment); - var activeCommentAfterSoftDelete = commentService.getSoftDeletedById(1L); - assertTrue(activeCommentAfterSoftDelete.isDeleted(), "Comment entity was soft deleted"); - assertEquals(allComments.size() - 1, commentService.list().size(), "Total records for comments"); - assertEquals(1, commentService.listSoftDeleted().size(), "Total deleted records for comments"); - - var deletedText = textService.getSoftDeletedById(1L); - textService.softUndelete(deletedText); - var deletedTextAfterSoftUndelete = textService.getById(1L); - assertTrue(deletedTextAfterSoftUndelete.isActive(), "Text entity was soft undeleted"); - assertEquals(allTexts.size(), textService.list().size(), "Total records for texts"); - assertEquals(0, textService.listSoftDeleted().size(), "Total deleted records for texts"); - - var deletedComment = commentService.getSoftDeletedById(1L); - commentService.softUndelete(deletedComment); - var deletedCommentAfterSoftUndelete = commentService.getById(1L); - assertFalse(deletedCommentAfterSoftUndelete.isDeleted(), "Comment entity was soft undeleted"); - assertEquals(allComments.size(), commentService.list().size(), "Total records for comments"); - assertEquals(0, commentService.listSoftDeleted().size(), "Total deleted records for comments"); - - textService.softDelete(allTexts); - assertEquals(0, textService.list().size(), "Total records for texts"); - assertEquals(allTexts.size(), textService.listSoftDeleted().size(), "Total deleted records for texts"); - - commentService.softDelete(allComments); - assertEquals(0, commentService.list().size(), "Total records for comments"); - assertEquals(allComments.size(), commentService.listSoftDeleted().size(), "Total deleted records for comments"); - } - - @Test - void testGetAllSoftDeletedForNonSoftDeletable() { - assertThrows(NonSoftDeletableEntityException.class, () -> personService.listSoftDeleted()); - } - - @Test - void testSoftDeleteNonSoftDeletable() { - var person = personService.getById(1L); - assertThrows(NonSoftDeletableEntityException.class, () -> personService.softDelete(person)); - } - - @Test - void testSoftUndeleteNonSoftDeletable() { - var person = personService.getById(1L); - assertThrows(NonSoftDeletableEntityException.class, () -> personService.softUndelete(person)); - } - - @Test - void testGetSoftDeletableById() { - lookupService.persist(new Lookup("aa")); - var activeLookup = lookupService.getById("aa"); - assertNotNull(activeLookup, "Got active entity with getById method"); - - lookupService.softDelete(activeLookup); - var softDeletedLookup = lookupService.getById("aa"); - assertNull(softDeletedLookup, "Not able to get deleted entity with getById method"); - - softDeletedLookup = lookupService.getSoftDeletedById("aa"); - assertNotNull(softDeletedLookup, "Got deleted entity with getSoftDeletedById method"); - } - - @Test - void testFindSoftDeletableById() { - lookupService.persist(new Lookup("bb")); - var activeLookup = lookupService.findById("bb"); - assertTrue(activeLookup.isPresent(), "Got active entity with findById method"); - - lookupService.softDelete(activeLookup.get()); - var softDeletedLookup = lookupService.findById("bb"); - assertFalse(softDeletedLookup.isPresent(), "Not able to get deleted entity with findById method"); - - softDeletedLookup = lookupService.findSoftDeletedById("bb"); - assertTrue(softDeletedLookup.isPresent(), "Got deleted entity with findSoftDeletedById method"); - } - - @Test - void testSave() { - var lookup = new Lookup("cc"); - lookupService.save(lookup); - var persistedLookup = lookupService.getById("cc"); - assertNotNull(persistedLookup, "New entity was persisted with save method"); - - persistedLookup.setActive(false); - lookupService.save(persistedLookup); - persistedLookup = lookupService.getSoftDeletedById("cc"); - assertFalse(persistedLookup.isActive(), "Entity was merged with save method"); - - persistedLookup.setActive(true); - lookupService.update(persistedLookup); - persistedLookup = lookupService.getById("cc"); - assertTrue(persistedLookup.isActive(), "Entity was merged with update method"); - } - - @Test - void testPersistExistingLookup() { - var lookup = new Lookup("dd"); - lookupService.save(lookup); - var persistedLookup = lookupService.getById("dd"); - persistedLookup.setActive(false); - assertThrows(IllegalEntityStateException.class, () -> lookupService.persist(lookup)); - } - - @Test - void testUpdateNewLookup() { - var lookup = new Lookup("ee"); - assertThrows(IllegalEntityStateException.class, () -> lookupService.update(lookup)); - } + // @SoftDeletable ------------------------------------------------------------------------------------------------- + + @Test + void testSoftDelete() { + var allTexts = textService.list(); + var allComments = commentService.list(); + + var activeText = textService.getById(1L); + textService.softDelete(activeText); + var activeTextAfterSoftDelete = textService.getSoftDeletedById(1L); + assertFalse(activeTextAfterSoftDelete.isActive(), "Text entity was soft deleted"); + assertEquals(allTexts.size() - 1, textService.list().size(), "Total records for texts"); + assertEquals(1, textService.listSoftDeleted().size(), "Total deleted records for texts"); + + var activeComment = commentService.getById(1L); + commentService.softDelete(activeComment); + var activeCommentAfterSoftDelete = commentService.getSoftDeletedById(1L); + assertTrue(activeCommentAfterSoftDelete.isDeleted(), "Comment entity was soft deleted"); + assertEquals(allComments.size() - 1, commentService.list().size(), "Total records for comments"); + assertEquals(1, commentService.listSoftDeleted().size(), "Total deleted records for comments"); + + var deletedText = textService.getSoftDeletedById(1L); + textService.softUndelete(deletedText); + var deletedTextAfterSoftUndelete = textService.getById(1L); + assertTrue(deletedTextAfterSoftUndelete.isActive(), "Text entity was soft undeleted"); + assertEquals(allTexts.size(), textService.list().size(), "Total records for texts"); + assertEquals(0, textService.listSoftDeleted().size(), "Total deleted records for texts"); + + var deletedComment = commentService.getSoftDeletedById(1L); + commentService.softUndelete(deletedComment); + var deletedCommentAfterSoftUndelete = commentService.getById(1L); + assertFalse(deletedCommentAfterSoftUndelete.isDeleted(), "Comment entity was soft undeleted"); + assertEquals(allComments.size(), commentService.list().size(), "Total records for comments"); + assertEquals(0, commentService.listSoftDeleted().size(), "Total deleted records for comments"); + + textService.softDelete(allTexts); + assertEquals(0, textService.list().size(), "Total records for texts"); + assertEquals(allTexts.size(), textService.listSoftDeleted().size(), "Total deleted records for texts"); + + commentService.softDelete(allComments); + assertEquals(0, commentService.list().size(), "Total records for comments"); + assertEquals(allComments.size(), commentService.listSoftDeleted().size(), "Total deleted records for comments"); + } + + @Test + void testGetAllSoftDeletedForNonSoftDeletable() { + assertThrows(NonSoftDeletableEntityException.class, () -> personService.listSoftDeleted()); + } + + @Test + void testSoftDeleteNonSoftDeletable() { + var person = personService.getById(1L); + assertThrows(NonSoftDeletableEntityException.class, () -> personService.softDelete(person)); + } + + @Test + void testSoftUndeleteNonSoftDeletable() { + var person = personService.getById(1L); + assertThrows(NonSoftDeletableEntityException.class, () -> personService.softUndelete(person)); + } + + @Test + void testGetSoftDeletableById() { + lookupService.persist(new Lookup("aa")); + var activeLookup = lookupService.getById("aa"); + assertNotNull(activeLookup, "Got active entity with getById method"); + + lookupService.softDelete(activeLookup); + var softDeletedLookup = lookupService.getById("aa"); + assertNull(softDeletedLookup, "Not able to get deleted entity with getById method"); + + softDeletedLookup = lookupService.getSoftDeletedById("aa"); + assertNotNull(softDeletedLookup, "Got deleted entity with getSoftDeletedById method"); + } + + @Test + void testFindSoftDeletableById() { + lookupService.persist(new Lookup("bb")); + var activeLookup = lookupService.findById("bb"); + assertTrue(activeLookup.isPresent(), "Got active entity with findById method"); + + lookupService.softDelete(activeLookup.get()); + var softDeletedLookup = lookupService.findById("bb"); + assertFalse(softDeletedLookup.isPresent(), "Not able to get deleted entity with findById method"); + + softDeletedLookup = lookupService.findSoftDeletedById("bb"); + assertTrue(softDeletedLookup.isPresent(), "Got deleted entity with findSoftDeletedById method"); + } + + @Test + void testSave() { + var lookup = new Lookup("cc"); + lookupService.save(lookup); + var persistedLookup = lookupService.getById("cc"); + assertNotNull(persistedLookup, "New entity was persisted with save method"); + + persistedLookup.setActive(false); + lookupService.save(persistedLookup); + persistedLookup = lookupService.getSoftDeletedById("cc"); + assertFalse(persistedLookup.isActive(), "Entity was merged with save method"); + + persistedLookup.setActive(true); + lookupService.update(persistedLookup); + persistedLookup = lookupService.getById("cc"); + assertTrue(persistedLookup.isActive(), "Entity was merged with update method"); + } + + @Test + void testPersistExistingLookup() { + var lookup = new Lookup("dd"); + lookupService.save(lookup); + var persistedLookup = lookupService.getById("dd"); + persistedLookup.setActive(false); + assertThrows(IllegalEntityStateException.class, () -> lookupService.persist(lookup)); + } + + @Test + void testUpdateNewLookup() { + var lookup = new Lookup("ee"); + assertThrows(IllegalEntityStateException.class, () -> lookupService.update(lookup)); + } } \ No newline at end of file diff --git a/src/test/java/org/omnifaces/persistence/test/model/Address.java b/src/test/java/org/omnifaces/persistence/test/model/Address.java index e9c4993..d7c64fe 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/Address.java +++ b/src/test/java/org/omnifaces/persistence/test/model/Address.java @@ -20,52 +20,52 @@ @Entity public class Address extends GeneratedIdEntity { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private @NotNull String street; - private @NotNull String houseNumber; - private @NotNull String postcode; - private @NotNull String city; - private @NotNull String country; + private @NotNull String street; + private @NotNull String houseNumber; + private @NotNull String postcode; + private @NotNull String city; + private @NotNull String country; - public String getStreet() { - return street; - } + public String getStreet() { + return street; + } - public void setStreet(String street) { - this.street = street; - } + public void setStreet(String street) { + this.street = street; + } - public String getHouseNumber() { - return houseNumber; - } + public String getHouseNumber() { + return houseNumber; + } - public void setHouseNumber(String houseNumber) { - this.houseNumber = houseNumber; - } + public void setHouseNumber(String houseNumber) { + this.houseNumber = houseNumber; + } - public String getPostcode() { - return postcode; - } + public String getPostcode() { + return postcode; + } - public void setPostcode(String postcode) { - this.postcode = postcode; - } + public void setPostcode(String postcode) { + this.postcode = postcode; + } - public String getCity() { - return city; - } + public String getCity() { + return city; + } - public void setCity(String city) { - this.city = city; - } + public void setCity(String city) { + this.city = city; + } - public String getCountry() { - return country; - } + public String getCountry() { + return country; + } - public void setCountry(String country) { - this.country = country; - } + public void setCountry(String country) { + this.country = country; + } } diff --git a/src/test/java/org/omnifaces/persistence/test/model/Comment.java b/src/test/java/org/omnifaces/persistence/test/model/Comment.java index 5f9800b..08a4a1a 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/Comment.java +++ b/src/test/java/org/omnifaces/persistence/test/model/Comment.java @@ -20,17 +20,17 @@ @Entity public class Comment extends GeneratedIdEntity { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @SoftDeletable - private boolean deleted; + @SoftDeletable + private boolean deleted; - public boolean isDeleted() { - return deleted; - } + public boolean isDeleted() { + return deleted; + } - public void setDeleted(boolean deleted) { - this.deleted = deleted; - } + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } } diff --git a/src/test/java/org/omnifaces/persistence/test/model/Gender.java b/src/test/java/org/omnifaces/persistence/test/model/Gender.java index 0a206c3..1165c63 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/Gender.java +++ b/src/test/java/org/omnifaces/persistence/test/model/Gender.java @@ -14,9 +14,9 @@ public enum Gender { - MALE, - FEMALE, - TRANS, - OTHER; + MALE, + FEMALE, + TRANS, + OTHER; } diff --git a/src/test/java/org/omnifaces/persistence/test/model/Group.java b/src/test/java/org/omnifaces/persistence/test/model/Group.java index c3fb1dc..c88829e 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/Group.java +++ b/src/test/java/org/omnifaces/persistence/test/model/Group.java @@ -14,9 +14,9 @@ public enum Group { - USER, - MANAGER, - ADMINISTRATOR, - DEVELOPER; + USER, + MANAGER, + ADMINISTRATOR, + DEVELOPER; } diff --git a/src/test/java/org/omnifaces/persistence/test/model/Lookup.java b/src/test/java/org/omnifaces/persistence/test/model/Lookup.java index ae2bbc6..bcecfc5 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/Lookup.java +++ b/src/test/java/org/omnifaces/persistence/test/model/Lookup.java @@ -24,39 +24,39 @@ @Entity public class Lookup extends BaseEntity { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @Id - @Column(length = 2, nullable = false, unique = true, name = "code") - private String id; + @Id + @Column(length = 2, nullable = false, unique = true, name = "code") + private String id; - @SoftDeletable(type = ACTIVE) - private boolean active = true; + @SoftDeletable(type = ACTIVE) + private boolean active = true; - public Lookup() { - // - } + public Lookup() { + // + } - public Lookup(String id) { - this.id = id; - } + public Lookup(String id) { + this.id = id; + } - @Override - public String getId() { - return id; - } + @Override + public String getId() { + return id; + } - @Override - public void setId(String id) { - this.id = id; - } + @Override + public void setId(String id) { + this.id = id; + } - public boolean isActive() { - return active; - } + public boolean isActive() { + return active; + } - public void setActive(boolean active) { - this.active = active; - } + public void setActive(boolean active) { + this.active = active; + } } diff --git a/src/test/java/org/omnifaces/persistence/test/model/Person.java b/src/test/java/org/omnifaces/persistence/test/model/Person.java index 5918dff..833d9b7 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/Person.java +++ b/src/test/java/org/omnifaces/persistence/test/model/Person.java @@ -35,68 +35,68 @@ @Entity public class Person extends GeneratedIdEntity { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private @NotNull String email; - private @NotNull @Enumerated Gender gender; - private @NotNull LocalDate dateOfBirth; + private @NotNull String email; + private @NotNull @Enumerated Gender gender; + private @NotNull LocalDate dateOfBirth; - @OneToOne(cascade=PERSIST, fetch=LAZY) - private Address address; + @OneToOne(cascade=PERSIST, fetch=LAZY) + private Address address; - @OneToMany(cascade=PERSIST) - private List phones = new ArrayList<>(); + @OneToMany(cascade=PERSIST) + private List phones = new ArrayList<>(); - @ElementCollection - @Column(name="\"groups\"") // "groups" has become a new reserved word since MySQL 8.0.2, so we need to quote it. - private @Enumerated(STRING) Set groups = new HashSet<>(); + @ElementCollection + @Column(name="\"groups\"") // "groups" has become a new reserved word since MySQL 8.0.2, so we need to quote it. + private @Enumerated(STRING) Set groups = new HashSet<>(); - public String getEmail() { - return email; - } + public String getEmail() { + return email; + } - public void setEmail(String email) { - this.email = email; - } + public void setEmail(String email) { + this.email = email; + } - public Gender getGender() { - return gender; - } + public Gender getGender() { + return gender; + } - public void setGender(Gender gender) { - this.gender = gender; - } + public void setGender(Gender gender) { + this.gender = gender; + } - public LocalDate getDateOfBirth() { - return dateOfBirth; - } + public LocalDate getDateOfBirth() { + return dateOfBirth; + } - public void setDateOfBirth(LocalDate dateOfBirth) { - this.dateOfBirth = dateOfBirth; - } + public void setDateOfBirth(LocalDate dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } - public Address getAddress() { - return address; - } + public Address getAddress() { + return address; + } - public void setAddress(Address address) { - this.address = address; - } + public void setAddress(Address address) { + this.address = address; + } - public List getPhones() { - return phones; - } + public List getPhones() { + return phones; + } - public void setPhones(List phones) { - this.phones = phones; - } + public void setPhones(List phones) { + this.phones = phones; + } - public Set getGroups() { - return groups; - } + public Set getGroups() { + return groups; + } - public void setGroups(Set groups) { - this.groups = groups; - } + public void setGroups(Set groups) { + this.groups = groups; + } } diff --git a/src/test/java/org/omnifaces/persistence/test/model/Phone.java b/src/test/java/org/omnifaces/persistence/test/model/Phone.java index dd541b0..bc045ad 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/Phone.java +++ b/src/test/java/org/omnifaces/persistence/test/model/Phone.java @@ -26,47 +26,47 @@ @Entity public class Phone extends GeneratedIdEntity { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public enum Type { - MOBILE, - HOME, - WORK; - } + public enum Type { + MOBILE, + HOME, + WORK; + } - private @NotNull @Enumerated(STRING) Type type; - private @NotNull String number; + private @NotNull @Enumerated(STRING) Type type; + private @NotNull String number; - @ManyToOne(optional=false, fetch=FetchType.LAZY) - private @NotNull Person owner; + @ManyToOne(optional=false, fetch=FetchType.LAZY) + private @NotNull Person owner; - public Type getType() { - return type; - } + public Type getType() { + return type; + } - public void setType(Type type) { - this.type = type; - } + public void setType(Type type) { + this.type = type; + } - public String getNumber() { - return number; - } + public String getNumber() { + return number; + } - public void setNumber(String number) { - this.number = number; - } + public void setNumber(String number) { + this.number = number; + } - public Person getOwner() { - return owner; - } + public Person getOwner() { + return owner; + } - public void setOwner(Person owner) { - this.owner = owner; - } + public void setOwner(Person owner) { + this.owner = owner; + } - @Transient - public String getEmail() { - return getOwner().getEmail(); - } + @Transient + public String getEmail() { + return getOwner().getEmail(); + } } diff --git a/src/test/java/org/omnifaces/persistence/test/model/Text.java b/src/test/java/org/omnifaces/persistence/test/model/Text.java index 386a898..6c34e6b 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/Text.java +++ b/src/test/java/org/omnifaces/persistence/test/model/Text.java @@ -22,17 +22,17 @@ @Entity public class Text extends GeneratedIdEntity { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - @SoftDeletable(type = ACTIVE) - private boolean active = true; + @SoftDeletable(type = ACTIVE) + private boolean active = true; - public boolean isActive() { - return active; - } + public boolean isActive() { + return active; + } - public void setActive(boolean active) { - this.active = active; - } + public void setActive(boolean active) { + this.active = active; + } } diff --git a/src/test/java/org/omnifaces/persistence/test/model/dto/PersonCard.java b/src/test/java/org/omnifaces/persistence/test/model/dto/PersonCard.java index 9cb170a..0ffee11 100644 --- a/src/test/java/org/omnifaces/persistence/test/model/dto/PersonCard.java +++ b/src/test/java/org/omnifaces/persistence/test/model/dto/PersonCard.java @@ -16,24 +16,24 @@ public class PersonCard extends Person { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private String addressString; - private Long totalPhones; + private String addressString; + private Long totalPhones; - public PersonCard(Long id, String email, String addressString, Long totalPhones) { - setId(id); - setEmail(email); - this.addressString = addressString; - this.totalPhones = totalPhones; - } + public PersonCard(Long id, String email, String addressString, Long totalPhones) { + setId(id); + setEmail(email); + this.addressString = addressString; + this.totalPhones = totalPhones; + } - public String getAddressString() { - return addressString; - } + public String getAddressString() { + return addressString; + } - public Long getTotalPhones() { - return totalPhones; - } + public Long getTotalPhones() { + return totalPhones; + } } diff --git a/src/test/java/org/omnifaces/persistence/test/service/PersonService.java b/src/test/java/org/omnifaces/persistence/test/service/PersonService.java index b67e3bb..8a6e9d0 100644 --- a/src/test/java/org/omnifaces/persistence/test/service/PersonService.java +++ b/src/test/java/org/omnifaces/persistence/test/service/PersonService.java @@ -32,48 +32,48 @@ @Stateless public class PersonService extends BaseEntityService { - public PartialResultList getPageWithAddress(Page page, boolean count) { - return getPage(page, count, "address"); - } - - public PartialResultList getPageWithPhones(Page page, boolean count) { - return getPage(page, count, "phones"); - } - - public PartialResultList getPageWithGroups(Page page, boolean count) { - return getPage(page, count, "groups"); - } - - public PartialResultList getPageOfPersonCards(Page page, boolean count) { - return getPage(page, count, PersonCard.class, (builder, query, person) -> { - Join personAddress = person.join("address"); - Join personPhones = person.join("phones"); - - LinkedHashMap, Expression> mapping = new LinkedHashMap<>(); - mapping.put(PersonCard::getId, person.get("id")); - mapping.put(PersonCard::getEmail, person.get("email")); - mapping.put(PersonCard::getAddressString, concat(builder, personAddress.get("street"), " ", personAddress.get("houseNumber"), ", ", personAddress.get("postcode"), " ", personAddress.get("city"), ", ", personAddress.get("country"))); - mapping.put(PersonCard::getTotalPhones, builder.count(personPhones)); - - query.groupBy(personAddress); - return mapping; - }); - } - - public PartialResultList getAllWithAddress() { - return getPageWithAddress(Page.ALL, false); - } - - public PartialResultList getAllWithPhones() { - return getPageWithPhones(Page.ALL, false); - } - - public PartialResultList getAllWithGroups() { - return getPageWithGroups(Page.ALL, false); - } - - public PartialResultList getAllPersonCards() { - return getPageOfPersonCards(Page.ALL, false); - } + public PartialResultList getPageWithAddress(Page page, boolean count) { + return getPage(page, count, "address"); + } + + public PartialResultList getPageWithPhones(Page page, boolean count) { + return getPage(page, count, "phones"); + } + + public PartialResultList getPageWithGroups(Page page, boolean count) { + return getPage(page, count, "groups"); + } + + public PartialResultList getPageOfPersonCards(Page page, boolean count) { + return getPage(page, count, PersonCard.class, (builder, query, person) -> { + Join personAddress = person.join("address"); + Join personPhones = person.join("phones"); + + LinkedHashMap, Expression> mapping = new LinkedHashMap<>(); + mapping.put(PersonCard::getId, person.get("id")); + mapping.put(PersonCard::getEmail, person.get("email")); + mapping.put(PersonCard::getAddressString, concat(builder, personAddress.get("street"), " ", personAddress.get("houseNumber"), ", ", personAddress.get("postcode"), " ", personAddress.get("city"), ", ", personAddress.get("country"))); + mapping.put(PersonCard::getTotalPhones, builder.count(personPhones)); + + query.groupBy(personAddress); + return mapping; + }); + } + + public PartialResultList getAllWithAddress() { + return getPageWithAddress(Page.ALL, false); + } + + public PartialResultList getAllWithPhones() { + return getPageWithPhones(Page.ALL, false); + } + + public PartialResultList getAllWithGroups() { + return getPageWithGroups(Page.ALL, false); + } + + public PartialResultList getAllPersonCards() { + return getPageOfPersonCards(Page.ALL, false); + } } diff --git a/src/test/java/org/omnifaces/persistence/test/service/PhoneService.java b/src/test/java/org/omnifaces/persistence/test/service/PhoneService.java index fedc595..5b29041 100644 --- a/src/test/java/org/omnifaces/persistence/test/service/PhoneService.java +++ b/src/test/java/org/omnifaces/persistence/test/service/PhoneService.java @@ -22,12 +22,12 @@ @Stateless public class PhoneService extends BaseEntityService { - public PartialResultList getPageWithOwners(Page page, boolean count) { - return getPage(page, count, "owner"); - } + public PartialResultList getPageWithOwners(Page page, boolean count) { + return getPage(page, count, "owner"); + } - public PartialResultList getAllWithOwners() { - return getPageWithOwners(Page.ALL, false); - } + public PartialResultList getAllWithOwners() { + return getPageWithOwners(Page.ALL, false); + } } \ No newline at end of file diff --git a/src/test/java/org/omnifaces/persistence/test/service/StartupService.java b/src/test/java/org/omnifaces/persistence/test/service/StartupService.java index 78f6ca7..a089653 100644 --- a/src/test/java/org/omnifaces/persistence/test/service/StartupService.java +++ b/src/test/java/org/omnifaces/persistence/test/service/StartupService.java @@ -36,70 +36,70 @@ @Singleton public class StartupService { - public static final int TOTAL_RECORDS = 200; - public static final int ROWS_PER_PAGE = 10; + public static final int TOTAL_RECORDS = 200; + public static final int ROWS_PER_PAGE = 10; public static final int TOTAL_PHONES_PER_PERSON_0 = 3; - @Inject - private TextService textService; - - @Inject - private CommentService commentService; - - @Inject - private PersonService personService; - - @PostConstruct - public void init() { - createTestPersons(); - createTestTexts(); - createTestComments(); - } - - private void createTestPersons() { - var genders = Gender.values(); - var phoneTypes = Phone.Type.values(); - var groups = Arrays.asList(Group.values()); - var random = ThreadLocalRandom.current(); - - for (var i = 0; i < TOTAL_RECORDS; i++) { - var person = new Person(); - person.setEmail("name" + i + "@example.com"); - person.setGender(genders[random.nextInt(genders.length)]); - person.setDateOfBirth(LocalDate.ofEpochDay(random.nextLong(LocalDate.of(1900, 1, 1).toEpochDay(), LocalDate.of(2000, 1, 1).toEpochDay()))); - - var address = new Address(); - address.setStreet("Street" + i); - address.setHouseNumber("" + i); - address.setPostcode("Postcode" + i); - address.setCity("City" + i); - address.setCountry("Country" + i); - person.setAddress(address); - - var totalPhones = i == 0 ? TOTAL_PHONES_PER_PERSON_0 : random.nextInt(1, 6); - for (var j = 0; j < totalPhones; j++) { - var phone = new Phone(); - phone.setType(phoneTypes[random.nextInt(phoneTypes.length)]); - phone.setNumber("0" + abs(random.nextInt())); - phone.setOwner(person); - person.getPhones().add(phone); - } - - Collections.shuffle(groups, random); - person.getGroups().addAll(groups.subList(0, random.nextInt(1, groups.size() + 1))); - - personService.persist(person); - } - } - - private void createTestTexts() { - textService.persist(new Text()); - textService.persist(new Text()); - } - - private void createTestComments() { - commentService.persist(new Comment()); - commentService.persist(new Comment()); - } + @Inject + private TextService textService; + + @Inject + private CommentService commentService; + + @Inject + private PersonService personService; + + @PostConstruct + public void init() { + createTestPersons(); + createTestTexts(); + createTestComments(); + } + + private void createTestPersons() { + var genders = Gender.values(); + var phoneTypes = Phone.Type.values(); + var groups = Arrays.asList(Group.values()); + var random = ThreadLocalRandom.current(); + + for (var i = 0; i < TOTAL_RECORDS; i++) { + var person = new Person(); + person.setEmail("name" + i + "@example.com"); + person.setGender(genders[random.nextInt(genders.length)]); + person.setDateOfBirth(LocalDate.ofEpochDay(random.nextLong(LocalDate.of(1900, 1, 1).toEpochDay(), LocalDate.of(2000, 1, 1).toEpochDay()))); + + var address = new Address(); + address.setStreet("Street" + i); + address.setHouseNumber("" + i); + address.setPostcode("Postcode" + i); + address.setCity("City" + i); + address.setCountry("Country" + i); + person.setAddress(address); + + var totalPhones = i == 0 ? TOTAL_PHONES_PER_PERSON_0 : random.nextInt(1, 6); + for (var j = 0; j < totalPhones; j++) { + var phone = new Phone(); + phone.setType(phoneTypes[random.nextInt(phoneTypes.length)]); + phone.setNumber("0" + abs(random.nextInt())); + phone.setOwner(person); + person.getPhones().add(phone); + } + + Collections.shuffle(groups, random); + person.getGroups().addAll(groups.subList(0, random.nextInt(1, groups.size() + 1))); + + personService.persist(person); + } + } + + private void createTestTexts() { + textService.persist(new Text()); + textService.persist(new Text()); + } + + private void createTestComments() { + commentService.persist(new Comment()); + commentService.persist(new Comment()); + } } diff --git a/src/test/resources/META-INF/persistence.xml b/src/test/resources/META-INF/persistence.xml index c528bca..0784a03 100644 --- a/src/test/resources/META-INF/persistence.xml +++ b/src/test/resources/META-INF/persistence.xml @@ -14,37 +14,37 @@ --> - - java:app/OmniPersistenceTest - - - - - - - - - - - - - - - - - - - - - - - - - - + + java:app/OmniPersistenceTest + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/web.xml b/src/test/resources/web.xml index 63633bd..8d6fed4 100644 --- a/src/test/resources/web.xml +++ b/src/test/resources/web.xml @@ -14,18 +14,18 @@ --> - OmniPersistenceTest + OmniPersistenceTest - - java:app/OmniPersistenceTest - org.h2.jdbcx.JdbcDataSource - jdbc:h2:mem:test - true - TRANSACTION_READ_COMMITTED - + + java:app/OmniPersistenceTest + org.h2.jdbcx.JdbcDataSource + jdbc:h2:mem:test + true + TRANSACTION_READ_COMMITTED + \ No newline at end of file