diff --git a/dev/io.openliberty.data.internal.persistence/resources/io/openliberty/data/internal/persistence/resources/CWWKDMessages.nlsprops b/dev/io.openliberty.data.internal.persistence/resources/io/openliberty/data/internal/persistence/resources/CWWKDMessages.nlsprops index 4afa191267f3..3bbaa7cd78da 100644 --- a/dev/io.openliberty.data.internal.persistence/resources/io/openliberty/data/internal/persistence/resources/CWWKDMessages.nlsprops +++ b/dev/io.openliberty.data.internal.persistence/resources/io/openliberty/data/internal/persistence/resources/CWWKDMessages.nlsprops @@ -1112,3 +1112,13 @@ CWWKD1109.jpa.anno.on.record.explanation=Jakarta Persistence annotations \ CWWKD1109.jpa.anno.on.record.useraction=Switch to a Jakarta Persistence \ entity class or remove the Jakarta Persistence annotation from the \ Java record. + +CWWKD1110.incompat.with.collection=CWWKD1110E: The {0} method of the \ + {1} repository interface has a name that includes the {2} keyword, which is \ + incompatible with the {3} attribute of the {4} entity because the attribute \ + has a collection type. The following Query by Method Name keywords can be \ + used with entity attributes that have a collection type: {5}. +CWWKD1110.incompat.with.collection.explanation=Some of the Query by Method Name \ + keywords are not compatible with entity attributes that have a collection type. +CWWKD1110.incompat.with.collection.useraction=Update the repository method \ + to avoid using the keyword that is not compatible with the collection type. diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/Condition.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/Condition.java index 4187bc3d462a..94891c675b1e 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/Condition.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/Condition.java @@ -12,87 +12,115 @@ *******************************************************************************/ package io.openliberty.data.internal.persistence; -import com.ibm.websphere.ras.annotation.Trivial; - -import jakarta.data.exceptions.MappingException; +import java.util.Set; +import java.util.TreeSet; /** + * Represents Query by Method Name condition keywords. */ enum Condition { - BETWEEN(null, 7, false), - CONTAINS(null, 8, true), - EMPTY(" IS EMPTY", 5, true), - ENDS_WITH(null, 8, false), - EQUALS("=", 0, true), - FALSE("=FALSE", 5, false), - GREATER_THAN(">", 11, false), - GREATER_THAN_EQUAL(">=", 16, false), - IN(" IN ", 2, false), - LESS_THAN("<", 8, false), - LESS_THAN_EQUAL("<=", 13, false), - LIKE(null, 4, false), - NOT_EMPTY(" IS NOT EMPTY", 8, true), - NOT_EQUALS("<>", 3, true), - NOT_NULL(" IS NOT NULL", 7, false), - NULL(" IS NULL", 4, false), - STARTS_WITH(null, 10, false), - TRUE("=TRUE", 4, false); + Between(null, false), + Contains(null, true), + Empty(" IS EMPTY", true), + EndsWith(null, false), + Equal("=", true), + False("=FALSE", false), + GreaterThan(">", false), + GreaterThanEqual(">=", false), + IgnoreCase(null, false), + In(" IN ", false), + LessThan("<", false), + LessThanEqual("<=", false), + Like(null, false), + Not("<>", true), + NotEmpty(" IS NOT EMPTY", true), + NotNull(" IS NOT NULL", false), + Null(" IS NULL", false), + StartsWith(null, false), + True("=TRUE", false); + /** + * Length of the Query by Method Name keyword. + */ final int length; + + /** + * Representation of the operator in query language. + */ final String operator; + + /** + * Indicates if this type of condition is supported on collections. + */ final boolean supportsCollections; - Condition(String operator, int length, boolean supportsCollections) { + /** + * Internal constructor for enumeration constants. + * + * @param operator Representation of the operator in query language. + * @param supportsCollections Indicates if this type of comparison is supported + * on collections. + */ + private Condition(String operator, boolean supportsCollections) { + int len = name().length(); this.operator = operator; - this.length = length; + this.length = len == 5 && name().equals("Equal") ? 0 : len; this.supportsCollections = supportsCollections; } + /** + * Returns the negated condition if possible. + * + * @return the negated comparison if possible. Otherwise null. + */ Condition negate() { switch (this) { - case EQUALS: - return NOT_EQUALS; - case GREATER_THAN: - return LESS_THAN_EQUAL; - case GREATER_THAN_EQUAL: - return LESS_THAN; - case LESS_THAN: - return GREATER_THAN_EQUAL; - case LESS_THAN_EQUAL: - return GREATER_THAN; - case NULL: - return NOT_NULL; - case TRUE: - return FALSE; - case FALSE: - return TRUE; - case EMPTY: - return NOT_EMPTY; - case NOT_EMPTY: - return EMPTY; - case NOT_EQUALS: - return EQUALS; - case NOT_NULL: - return NULL; + case Equal: + return Not; + case GreaterThan: + return LessThanEqual; + case GreaterThanEqual: + return LessThan; + case LessThan: + return GreaterThanEqual; + case LessThanEqual: + return GreaterThan; + case Null: + return NotNull; + case True: + return False; + case False: + return True; + case Empty: + return NotEmpty; + case Not: + return Equal; + case NotEmpty: + return Empty; + case NotNull: + return Null; default: return null; } } /** - * Confirm that collections are supported for this condition, - * based on whether case insensitive comparison is requested. + * Returns names of all conditions that are supported for collection attributes. + * This is used in error reporting to display which keywords are valid. * - * @param attributeName entity attribute to which the condition is to be applied. - * @param ignoreCase indicates if the condition is to be performed ignoring case. - * @throws MappingException with chained UnsupportedOperationException if not supported. + * @return names of all conditions that are supported for collection attributes. */ - @Trivial - void verifyCollectionsSupported(String attributeName, boolean ignoreCase) { - if (!supportsCollections || ignoreCase) - throw new MappingException(new UnsupportedOperationException("Repository keyword " + - (ignoreCase ? "IgnoreCase" : name()) + - " which is applied to entity attribute " + attributeName + - " is not supported for collection attributes.")); // TODO + static Set supportedForCollections() { + Set supported = new TreeSet<>(); + for (Condition c : Condition.values()) + if (c.supportsCollections && c.length > 0) { + String name = c.name(); + supported.add(name); + // Some negated forms of keywords do not have constants in this + // enumeration, but they can be formed by combining with Not: + if (c.negate() == null && !name.startsWith(Not.name())) + supported.add(Not.name() + name); + } + return supported; } } diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/CursoredPageImpl.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/CursoredPageImpl.java index c780ade9e23c..204484929432 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/CursoredPageImpl.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/CursoredPageImpl.java @@ -47,13 +47,47 @@ public class CursoredPageImpl implements CursoredPage { private static final TraceComponent tc = Tr.register(CursoredPageImpl.class); + /** + * Values that are supplied when invoking the repository method that + * requests the cursored page. + */ private final Object[] args; + + /** + * Indicates the direction of pagination relative to a cursor. + * In the case of a first page requested with offset pagination, + * where there is no cursor, the direction is forward. + */ private final boolean isForward; + + /** + * The request for this page. + */ private final PageRequest pageRequest; + + /** + * Query information. + */ private final QueryInfo queryInfo; + + /** + * Results of the query for this page. + */ private final List results; + + /** + * Total number of elements across all pages. This value is computed lazily, + * with -1 indicating it has not been computed yet. + */ private long totalElements = -1; + /** + * Construct a new CursoredPage. + * + * @param queryInfo query information. + * @param pageRequest the request for this page. + * @param args values that are supplied to the repository method. + */ @FFDCIgnore(Exception.class) @Trivial // avoid tracing customer data CursoredPageImpl(QueryInfo queryInfo, PageRequest pageRequest, Object[] args) { diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/PageImpl.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/PageImpl.java index 609ad1d3d9ce..6cecef9d8677 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/PageImpl.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/PageImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022,2024 IBM Corporation and others. + * Copyright (c) 2022,2025 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -39,12 +39,40 @@ public class PageImpl implements Page { private static final TraceComponent tc = Tr.register(PageImpl.class); + /** + * Values that are supplied when invoking the repository method that + * requests the page. + */ private final Object[] args; + + /** + * The request for this page. + */ private final PageRequest pageRequest; + + /** + * Query information. + */ private final QueryInfo queryInfo; + + /** + * Results of the query for this page. + */ private final List results; + + /** + * Total number of elements across all pages. This value is computed lazily, + * with -1 indicating it has not been computed yet. + */ private long totalElements = -1; + /** + * Construct a new Page. + * + * @param queryInfo query information. + * @param pageRequest the request for this page. + * @param args values that are supplied to the repository method. + */ @FFDCIgnore(Exception.class) @Trivial PageImpl(QueryInfo queryInfo, PageRequest pageRequest, Object[] args) { diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java index d43b7406e8fa..76b701b04672 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java @@ -12,6 +12,8 @@ *******************************************************************************/ package io.openliberty.data.internal.persistence; +import static io.openliberty.data.internal.persistence.Condition.IgnoreCase; +import static io.openliberty.data.internal.persistence.Condition.Not; import static io.openliberty.data.internal.persistence.Util.SORT_PARAM_TYPES; import static io.openliberty.data.internal.persistence.Util.lifeCycleReturnTypes; import static io.openliberty.data.internal.persistence.cdi.DataExtension.exc; @@ -2299,23 +2301,27 @@ private LinkedHashSet findNamedParameters(String ql, int startAt) { /** * Generates JPQL for a *By condition such as MyColumn[IgnoreCase][Not]Like */ - private void generateCondition(String methodName, int start, int endBefore, StringBuilder q) { + private void generateCondition(String methodName, + int start, + int endBefore, + StringBuilder q) { int length = endBefore - start; - Condition condition = Condition.EQUALS; + Condition condition = Condition.Equal; switch (methodName.charAt(endBefore - 1)) { case 'n': // GreaterThan | LessThan | In | Between if (length > 2) { char ch = methodName.charAt(endBefore - 2); if (ch == 'a') { // GreaterThan | LessThan if (endsWith("GreaterTh", methodName, start, endBefore - 2)) - condition = Condition.GREATER_THAN; + condition = Condition.GreaterThan; else if (endsWith("LessTh", methodName, start, endBefore - 2)) - condition = Condition.LESS_THAN; + condition = Condition.LessThan; } else if (ch == 'I') { // In - condition = Condition.IN; - } else if (ch == 'e' && endsWith("Betwe", methodName, start, endBefore - 2)) { - condition = Condition.BETWEEN; + condition = Condition.In; + } else if (ch == 'e' && + endsWith("Betwe", methodName, start, endBefore - 2)) { + condition = Condition.Between; } } break; @@ -2324,11 +2330,13 @@ else if (endsWith("LessTh", methodName, start, endBefore - 2)) char ch = methodName.charAt(endBefore - 2); if (ch == 'a') { // GreaterThanEqual | LessThanEqual if (endsWith("GreaterThanEqu", methodName, start, endBefore - 2)) - condition = Condition.GREATER_THAN_EQUAL; + condition = Condition.GreaterThanEqual; else if (endsWith("LessThanEqu", methodName, start, endBefore - 2)) - condition = Condition.LESS_THAN_EQUAL; - } else if (ch == 'l' & methodName.charAt(endBefore - 3) == 'u' && methodName.charAt(endBefore - 4) == 'N') { - condition = Condition.NULL; + condition = Condition.LessThanEqual; + } else if (ch == 'l' && + methodName.charAt(endBefore - 3) == 'u' && + methodName.charAt(endBefore - 4) == 'N') { + condition = Condition.Null; } } break; @@ -2336,13 +2344,15 @@ else if (endsWith("LessThanEqu", methodName, start, endBefore - 2)) if (length > 4) { char ch = methodName.charAt(endBefore - 4); if (ch == 'L') { - if (methodName.charAt(endBefore - 3) == 'i' && methodName.charAt(endBefore - 2) == 'k') - condition = Condition.LIKE; + if (methodName.charAt(endBefore - 3) == 'i' && + methodName.charAt(endBefore - 2) == 'k') + condition = Condition.Like; } else if (ch == 'T') { - if (methodName.charAt(endBefore - 3) == 'r' && methodName.charAt(endBefore - 2) == 'u') - condition = Condition.TRUE; + if (methodName.charAt(endBefore - 3) == 'r' && + methodName.charAt(endBefore - 2) == 'u') + condition = Condition.True; } else if (endsWith("Fals", methodName, start, endBefore - 1)) { - condition = Condition.FALSE; + condition = Condition.False; } } break; @@ -2351,27 +2361,28 @@ else if (endsWith("LessThanEqu", methodName, start, endBefore - 2)) char ch = methodName.charAt(endBefore - 8); if (ch == 'E') { if (endsWith("ndsWit", methodName, start, endBefore - 1)) - condition = Condition.ENDS_WITH; - } else if (endBefore > 10 && ch == 'a' && endsWith("StartsWit", methodName, start, endBefore - 1)) { - condition = Condition.STARTS_WITH; + condition = Condition.EndsWith; + } else if (endBefore > 10 && ch == 'a' && + endsWith("StartsWit", methodName, start, endBefore - 1)) { + condition = Condition.StartsWith; } } break; case 's': // Contains if (endsWith("Contain", methodName, start, endBefore - 1)) - condition = Condition.CONTAINS; + condition = Condition.Contains; break; case 'y': // Empty if (endsWith("Empt", methodName, start, endBefore - 1)) - condition = Condition.EMPTY; + condition = Condition.Empty; } endBefore -= condition.length; - boolean negated = endsWith("Not", methodName, start, endBefore); + boolean negated = endsWith(Not.name(), methodName, start, endBefore); endBefore -= (negated ? 3 : 0); - boolean ignoreCase = endsWith("IgnoreCase", methodName, start, endBefore); + boolean ignoreCase = endsWith(IgnoreCase.name(), methodName, start, endBefore); endBefore -= (ignoreCase ? 10 : 0); String attribute = methodName.substring(start, endBefore); @@ -2399,57 +2410,82 @@ else if (endsWith("LessThanEqu", methodName, start, endBefore - 2)) } boolean isCollection = entityInfo.collectionElementTypes.containsKey(name); - if (isCollection) - condition.verifyCollectionsSupported(name, ignoreCase); + if (isCollection && (ignoreCase || !condition.supportsCollections)) + throw exc(MappingException.class, + "CWWKD1110.incompat.with.collection", + method.getName(), + repositoryInterface.getName(), + ignoreCase ? IgnoreCase.name() : condition.name(), + name, + entityInfo.getType().getName(), + Condition.supportedForCollections()); switch (condition) { - case STARTS_WITH: - q.append(attributeExpr).append(negated ? " NOT " : " ").append("LIKE CONCAT("); + case StartsWith: + q.append(attributeExpr) // + .append(negated ? " NOT " : " ") // + .append("LIKE CONCAT("); generateParam(q, ignoreCase, ++jpqlParamCount).append(", '%')"); break; - case ENDS_WITH: - q.append(attributeExpr).append(negated ? " NOT " : " ").append("LIKE CONCAT('%', "); + case EndsWith: + q.append(attributeExpr) // + .append(negated ? " NOT " : " ") // + .append("LIKE CONCAT('%', "); generateParam(q, ignoreCase, ++jpqlParamCount).append(")"); break; - case LIKE: - q.append(attributeExpr).append(negated ? " NOT " : " ").append("LIKE "); + case Like: + q.append(attributeExpr) // + .append(negated ? " NOT " : " ") // + .append("LIKE "); generateParam(q, ignoreCase, ++jpqlParamCount); break; - case BETWEEN: - q.append(attributeExpr).append(negated ? " NOT " : " ").append("BETWEEN "); + case Between: + q.append(attributeExpr) // + .append(negated ? " NOT " : " ") // + .append("BETWEEN "); generateParam(q, ignoreCase, ++jpqlParamCount).append(" AND "); generateParam(q, ignoreCase, ++jpqlParamCount); break; - case CONTAINS: + case Contains: if (isCollection) { - q.append(" ?").append(++jpqlParamCount).append(negated ? " NOT " : " ").append("MEMBER OF ").append(attributeExpr); + q.append(" ?").append(++jpqlParamCount) // + .append(negated ? " NOT " : " ") // + .append("MEMBER OF ").append(attributeExpr); } else { - q.append(attributeExpr).append(negated ? " NOT " : " ").append("LIKE CONCAT('%', "); + q.append(attributeExpr) // + .append(negated ? " NOT " : " ") // + .append("LIKE CONCAT('%', "); generateParam(q, ignoreCase, ++jpqlParamCount).append(", '%')"); } break; - case NULL: - case NOT_NULL: - case TRUE: - case FALSE: + case Null: + case NotNull: + case True: + case False: q.append(attributeExpr).append(condition.operator); break; - case EMPTY: - q.append(attributeExpr).append(isCollection ? Condition.EMPTY.operator : Condition.NULL.operator); + case Empty: + q.append(attributeExpr).append(isCollection // + ? Condition.Empty.operator // + : Condition.Null.operator); break; - case NOT_EMPTY: - q.append(attributeExpr).append(isCollection ? Condition.NOT_EMPTY.operator : Condition.NOT_NULL.operator); + case NotEmpty: + q.append(attributeExpr).append(isCollection // + ? Condition.NotEmpty.operator // + : Condition.NotNull.operator); break; - case IN: + case In: if (ignoreCase) throw exc(UnsupportedOperationException.class, "CWWKD1074.qbmn.incompat.keywords", method.getName(), repositoryInterface.getName(), - "IgnoreCase", - "In"); + IgnoreCase.name(), + condition.name()); default: - q.append(attributeExpr).append(negated ? " NOT " : "").append(condition.operator); + q.append(attributeExpr) // + .append(negated ? " NOT " : "") // + .append(condition.operator); generateParam(q, ignoreCase, ++jpqlParamCount); } } @@ -4901,7 +4937,7 @@ private void parseOrderBy(int orderBy, StringBuilder q) { boolean ignoreCase; boolean descending = iNext > 0 && iNext == desc; int endBefore = iNext < 0 ? methodName.length() : iNext; - if (ignoreCase = endsWith("IgnoreCase", methodName, i, endBefore)) + if (ignoreCase = endsWith(IgnoreCase.name(), methodName, i, endBefore)) endBefore -= 10; String attribute = methodName.substring(i, endBefore); diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java index 4783774878a4..e9a363854f56 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java @@ -16,7 +16,6 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; -import java.lang.reflect.Parameter; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLIntegrityConstraintViolationException; @@ -63,18 +62,68 @@ import jakarta.persistence.Table; import jakarta.transaction.Status; +/** + * Provides implementation of the methods of a repository interface. + * + * @param repository interface. + */ public class RepositoryImpl implements InvocationHandler { private static final TraceComponent tc = Tr.register(RepositoryImpl.class); - private static final ThreadLocal> defaultMethodResources = new ThreadLocal<>(); + /** + * Keeps track of resources that were obtained via resource accessor methods + * from a repository default method, so that when the default method ends, + * the resources can be automatically closed if they implement AutoCloseable. + */ + private static final ThreadLocal> defaultMethodResources = // + new ThreadLocal<>(); + /** + * Indicates if the bean for the repository has been disposed. + */ private final AtomicBoolean isDisposed = new AtomicBoolean(); + + /** + * Entity information for the primary entity type of the repository. + * Null if the repository does not have a primary entity type. + */ final CompletableFuture primaryEntityInfoFuture; + + /** + * OSGi service for the built-in Jakarta Data provider for EclipseLink. + */ final DataProvider provider; - final Map> queries = new HashMap<>(); + + /** + * Mapping of repository interface method to a future for the initialized + * state of the information that is needed to perform the query. + */ + private final Map> queries = new HashMap<>(); + + /** + * The repository interface that implementation is provided for. + */ final Class repositoryInterface; + + /** + * Abstraction for a Jakarta Validation Validator. + */ final EntityValidator validator; + /** + * Construct a new instance. + * + * @param provider OSGi service for the built-in Jakarta Data + * provider for EclipseLink. + * @param extension CDI extension for the Jakarta Data provider. + * @param builder Builder of EntityManager instances. + * @param repositoryInterface The repository interface. + * @param primaryEntityClass The primary entity class for the repository. + * Null if the repository does not have one. + * @param queriesPerEntityClass Map of entity class to a list of the query + * information for each repository method + * that operates on the entity. + */ public RepositoryImpl(DataProvider provider, DataExtension extension, EntityManagerBuilder builder, @@ -324,20 +373,6 @@ else if (UnsupportedOperationException.class.equals(cause.getClass())) return x; } - /** - * Return a name for the parameter, suitable for display in an NLS message. - * - * @param param parameter - * @param index zero-based method index. - * @return parameter name. - */ - @Trivial - private static final String getName(Parameter param, int index) { - return param.isNamePresent() // - ? param.getName() // - : ("(" + (index + 1) + ")"); - } - /** * Used during introspection to report errors that occurred when processing * repository methods. @@ -413,6 +448,17 @@ else if (Connection.class.equals(type)) return t; } + /** + * Provides the implementation of repository interface methods. + * + * @param proxy instance upon which the method is invoked. + * @param method repository interface method to implement. + * @param args arguments that are supplied to the repository method. + * @throws Throwable if an error occurs. Typically this will be a DataException, + * a subclass of DataException, or a subclass of + * RuntimeException, as determined by the Jakarta Data + * specification and API. + */ @FFDCIgnore(Throwable.class) @Override @Trivial @@ -425,12 +471,14 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (method.isDefault()) { isDefaultMethod = true; } else { + // Special case handling of various methods from java.lang.Object: String methodName = method.getName(); if (args == null) { if ("hashCode".equals(methodName)) return System.identityHashCode(proxy); else if ("toString".equals(methodName)) - return repositoryInterface.getName() + "(Proxy)@" + Integer.toHexString(System.identityHashCode(proxy)); + return repositoryInterface.getName() + "(Proxy)@" + + Integer.toHexString(System.identityHashCode(proxy)); } else if (args.length == 1) { if ("equals".equals(methodName)) return proxy == args[0]; @@ -440,7 +488,8 @@ else if ("toString".equals(methodName)) final boolean trace = TraceComponent.isAnyTracingEnabled(); if (trace && tc.isEntryEnabled()) - Tr.entry(this, tc, "invoke " + repositoryInterface.getSimpleName() + '.' + method.getName(), + Tr.entry(this, tc, "invoke " + repositoryInterface.getSimpleName() + + '.' + method.getName(), provider.loggable(repositoryInterface, method, args)); EntityInfo entityInfo = null; @@ -466,7 +515,9 @@ else if ("toString".equals(methodName)) try { Object returnValue = InvocationHandler.invokeDefault(proxy, method, args); if (trace && tc.isEntryEnabled()) - Tr.exit(this, tc, "invoke " + repositoryInterface.getSimpleName() + '.' + method.getName(), returnValue); + Tr.exit(this, tc, "invoke " + repositoryInterface.getSimpleName() + + '.' + method.getName(), + returnValue); return returnValue; } finally { for (AutoCloseable resource; (resource = resourceStack.pollLast()) != null;) @@ -558,7 +609,8 @@ else if (status != Status.STATUS_NO_TRANSACTION) Object valueToLog = hideValue // ? provider.loggable(repositoryInterface, method, returnValue) // : returnValue; - Tr.exit(this, tc, "invoke " + repositoryInterface.getSimpleName() + '.' + method.getName(), + Tr.exit(this, tc, "invoke " + repositoryInterface.getSimpleName() + + '.' + method.getName(), valueToLog); } return returnValue; @@ -566,7 +618,9 @@ else if (status != Status.STATUS_NO_TRANSACTION) if (!isDefaultMethod && x instanceof Exception) x = failure((Exception) x, entityInfo == null ? null : entityInfo.builder); if (trace && tc.isEntryEnabled()) - Tr.exit(this, tc, "invoke " + repositoryInterface.getSimpleName() + '.' + method.getName(), x); + Tr.exit(this, tc, "invoke " + repositoryInterface.getSimpleName() + + '.' + method.getName(), + x); throw x; } } diff --git a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java index 86bd0825b4d4..80f1df526c26 100644 --- a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java +++ b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java @@ -3834,6 +3834,52 @@ public void testNulls() { .collect(Collectors.toList())); } + /** + * Test implementation of methods that a repository inherits from + * java.lang.Object, some of which go through the proxy handler. + */ + @SuppressWarnings("unlikely-arg-type") + @Test + public void testObjectMethods() throws InterruptedException { + // .equals is true for same instance and false for other instance + assertEquals(true, people.equals(people)); + assertEquals(false, people.equals(personnel)); + + // .getClass returns the repository interface + assertEquals(true, People.class.isAssignableFrom(people.getClass())); + + // .hashCode returns same value each time invoked + int hash = people.hashCode(); + assertEquals(hash, people.hashCode()); + + // .notify and .notifyAll + synchronized (people) { + people.notify(); + people.notifyAll(); + } + + // .toString + String str = people.toString(); + assertEquals(str, + true, + str.contains(People.class.getName())); + + // .wait + synchronized (people) { + people.wait(20); // 20 ms + people.wait(10, 500000); // 10.5 ms + Thread.currentThread().interrupt(); + try { + // wait until interrupted, which should be immediately per above + people.wait(); + } catch (InterruptedException x) { + // expected + } finally { + Thread.interrupted(); + } + } + } + /** * Verify a repository method that supplies id(this) as the sort criteria * hard coded within a JDQL query. diff --git a/dev/io.openliberty.data.internal_fat_errorpaths/fat/src/test/jakarta/data/errpaths/DataErrPathsTest.java b/dev/io.openliberty.data.internal_fat_errorpaths/fat/src/test/jakarta/data/errpaths/DataErrPathsTest.java index 7fa85a8ebec7..e4c18e39364c 100644 --- a/dev/io.openliberty.data.internal_fat_errorpaths/fat/src/test/jakarta/data/errpaths/DataErrPathsTest.java +++ b/dev/io.openliberty.data.internal_fat_errorpaths/fat/src/test/jakarta/data/errpaths/DataErrPathsTest.java @@ -118,7 +118,9 @@ public class DataErrPathsTest extends FATServletClient { "CWWKD1104E.*inWard", // @Param with empty string value "CWWKD1105E.*findByNameNotNullOrderByDescriptionAsc", // keyword in OrderBy "CWWKD1108E.*Invitation", // JPA entity lacks @Entity - "CWWKD1109E.*Investment" // Record entity has JPA anno + "CWWKD1109E.*Investment", // Record entity has JPA anno + "CWWKD1110E.*findByEmailAddressesGreaterThanEqual", // collection >= + "CWWKD1110E.*findByEmailAddressesIgnoreCaseContains" // collection IgnoreCase }; @Server("io.openliberty.data.internal.fat.errpaths") diff --git a/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/DataErrPathsTestServlet.java b/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/DataErrPathsTestServlet.java index 9f2d8583ffd8..9eda4bd1daa7 100644 --- a/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/DataErrPathsTestServlet.java +++ b/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/DataErrPathsTestServlet.java @@ -146,11 +146,14 @@ public void init(ServletConfig config) throws ServletException { em.persist(new Voter(987665432, "Vivian", // LocalDate.of(1971, Month.OCTOBER, 1), // - "701 Silver Creek Rd NE, Rochester, MN 55906")); + "701 Silver Creek Rd NE, Rochester, MN 55906", // + "vivian@openliberty.io", // + "vivian.voter@openliberty.io")); em.persist(new Voter(789001234, "Vincent", // LocalDate.of(1977, Month.SEPTEMBER, 26), // - "770 W Silver Lake Dr NE, Rochester, MN 55906")); + "770 W Silver Lake Dr NE, Rochester, MN 55906", // + "vincent@openliberty.io")); } finally { tx.commit(); } @@ -181,6 +184,44 @@ public void testBothNamedAndPositionalParameters() { } } + /** + * Verify an error is raised when the GreaterThanEqual keyword is applied to a + * collection of values. + */ + @Test + public void testCollectionGreaterThanEqual() { + try { + List found = voters.findByEmailAddressesGreaterThanEqual(1); + fail("Should not be able to compare a collection to a number." + + " Found " + found); + } catch (MappingException x) { + if (x.getMessage() == null || + !x.getMessage().startsWith("CWWKD1110E:") || + !x.getMessage().contains("GreaterThanEqual")) + throw x; + } + } + + /** + * Verify an error is raised when the IgnoreCase keyword is applied to a + * collection of values. + */ + @Test + public void testCollectionIgnoreCase() { + List found; + try { + String mixedCaseEmail = "Vivian@OpenLiberty.io"; + found = voters.findByEmailAddressesIgnoreCaseContains(mixedCaseEmail); + fail("Should not be able to compare a collection ignoring case." + + " Found " + found); + } catch (MappingException x) { + if (x.getMessage() == null || + !x.getMessage().startsWith("CWWKD1110E:") || + !x.getMessage().contains("IgnoreCase")) + throw x; + } + } + /** * Verify an error is raised when a value cannot be safely converted to byte. */ @@ -991,10 +1032,12 @@ public void testFindAndDeleteReturnsInvalidTypesEmpty() { public void testInsertMultipleEntitiesButOnlyReturnOne() { Voter v1 = new Voter(100200300, "Valerie", // LocalDate.of(1947, Month.NOVEMBER, 7), // - "88 23rd Ave SW, Rochester, MN 55902"); + "88 23rd Ave SW, Rochester, MN 55902", // + "valerie@openliberty.io"); Voter v2 = new Voter(400500600, "Vinny", // LocalDate.of(1988, Month.NOVEMBER, 8), // - "2016 45th St SE, Rochester, MN 55904"); + "2016 45th St SE, Rochester, MN 55904", // + "vinny@openliberty.io"); try { Voter inserted = voters.register(v1, v2); fail("Insert method with singular return type should not be able to " + @@ -1161,11 +1204,13 @@ public void testLifeCycleInsertMethodWithMultipleParameters() { List list = List // .of(new Voter(999887777, "New Voter 1", // LocalDate.of(1999, Month.DECEMBER, 9), // - "213 13th Ave NW, Rochester, MN 55901"), + "213 13th Ave NW, Rochester, MN 55901", // + "voter1@openliberty.io"), new Voter(777665555, "New Voter 2", // LocalDate.of(1987, Month.NOVEMBER, 7), // - "300 7th St SW, Rochester, MN 55902")); + "300 7th St SW, Rochester, MN 55902", // + "voter2@openliberty.io")); try { list = voters.addSome(list, Limit.of(1)); diff --git a/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/Voter.java b/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/Voter.java index 5dbfc3dad9b2..dec01d30d35b 100644 --- a/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/Voter.java +++ b/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/Voter.java @@ -13,9 +13,13 @@ package test.jakarta.data.errpaths.web; import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; /** @@ -34,6 +38,9 @@ public class Voter { public String description; + @ElementCollection(fetch = FetchType.EAGER) + public Set emailAddresses = new HashSet<>(); + @Id @Column(nullable = false) public int ssn; @@ -41,12 +48,18 @@ public class Voter { public Voter() { } - public Voter(int ssn, String name, LocalDate birthday, String address) { + public Voter(int ssn, + String name, + LocalDate birthday, + String address, + String... emailAddresses) { this.ssn = ssn; this.name = name; this.birthday = birthday; this.address = address; this.description = name + " born on " + birthday + " and living at " + address; + for (String email : emailAddresses) + this.emailAddresses.add(email); } @Override @@ -56,6 +69,7 @@ public int hashCode() { @Override public String toString() { - return "Voter#" + ssn + " " + birthday + " " + name + " @" + address; + return "Voter#" + ssn + " " + birthday + " " + name + + " @" + address + " " + emailAddresses; } } diff --git a/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/Voters.java b/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/Voters.java index 9b1b070a93c7..369cd48e64cc 100644 --- a/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/Voters.java +++ b/dev/io.openliberty.data.internal_fat_errorpaths/test-applications/DataErrPathsTestApp/src/test/jakarta/data/errpaths/web/Voters.java @@ -231,6 +231,17 @@ List deleteReturnStringByAddress(String address, CursoredPage findByBirthdayOrderBySSN(LocalDate birthday, PageRequest pageReq); + /** + * This invalid method tries to apply the GreaterThanEqual keyword to a + * collection of values. + */ + List findByEmailAddressesGreaterThanEqual(int minEmailAddresses); + + /** + * This invalid method tries to apply the IgnoreCase keyword to a collection. + */ + List findByEmailAddressesIgnoreCaseContains(String email); + /** * This invalid method omits the entity attribute name from findBy in the * method name.