| /* |
| * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0, |
| * or the Eclipse Distribution License v. 1.0 which is available at |
| * http://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause |
| */ |
| |
| // Contributors: |
| // Oracle - initial API and implementation |
| // |
| package org.eclipse.persistence.jpa.tests.jpql.tools; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import jakarta.persistence.AccessType; |
| import jpql.query.Address; |
| import jpql.query.Employee; |
| import jpql.query.EnumType; |
| import jpql.query.Project; |
| import org.eclipse.persistence.jpa.jpql.ExpressionTools; |
| import org.eclipse.persistence.jpa.jpql.JPAVersion; |
| import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar; |
| import org.eclipse.persistence.jpa.jpql.tools.AbstractJPQLQueryHelper; |
| import org.eclipse.persistence.jpa.jpql.tools.ContentAssistExtension; |
| import org.eclipse.persistence.jpa.jpql.tools.ContentAssistProposals; |
| import org.eclipse.persistence.jpa.jpql.tools.ContentAssistProposals.EnumProposals; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IEntity; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedType; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IMapping; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IType; |
| import org.eclipse.persistence.jpa.jpql.utility.CollectionTools; |
| import org.eclipse.persistence.jpa.tests.jpql.JPQLCoreTest; |
| import org.eclipse.persistence.jpa.tests.jpql.JPQLQueryHelperTestHelper; |
| import org.eclipse.persistence.jpa.tests.jpql.parser.JPQLQueryBNFAccessor; |
| import org.eclipse.persistence.jpa.tests.jpql.tools.spi.java.JavaQuery; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.*; |
| import static org.junit.Assert.*; |
| |
| /** |
| * The abstract unit-test providing helper methods required for testing content assist. |
| * |
| * @version 2.5 |
| * @since 2.5 |
| * @author Pascal Filion |
| */ |
| @SuppressWarnings("nls") |
| public abstract class ContentAssistTest extends JPQLCoreTest { |
| |
| protected JPQLQueryBNFAccessor bnfAccessor; |
| @JPQLQueryHelperTestHelper |
| protected AbstractJPQLQueryHelper queryHelper; |
| protected JavaQuery virtualQuery; |
| |
| /** |
| * Returns the return type associated with the given JPQL identifier. Only JPQL identifiers mark |
| * with {@link org.eclipse.persistence.jpa.jpql.parser.IdentifierRole#FUNCTION} have a return |
| * type. |
| * |
| * @param identifier The JPQL identifier for which its expected return type should be returned |
| * @return Either the return type for the given JPQL identifier or <code>null</code> if the |
| * expression does not return a value |
| */ |
| protected Class<?> acceptableType(String identifier) { |
| return null; |
| } |
| |
| /** |
| * Creates a new {@link ContentAssistExtension} that can be used to provide additional |
| * information that is outside the scope of simply providing JPA metadata information, |
| * such as table names, column names, class names. |
| * |
| * @return By default, {@link ContentAssistExtension#NULL_HELPER} is returned |
| */ |
| protected ContentAssistExtension buildContentAssistExtension() { |
| return ContentAssistExtension.NULL_HELPER; |
| } |
| |
| /** |
| * Retrieves the possibles choices that can complete the query from the given position of the |
| * cursor within the JPQL query. |
| * |
| * @param jpqlQuery The JPQL query to parse and for which content assist proposals will be |
| * calculated based on the position of the cursor within that query |
| * @param position The position of the cursor within the given JPQL query |
| * @return The list of valid proposals regrouped by categories |
| */ |
| protected final ContentAssistProposals buildContentAssistProposals(String jpqlQuery, int position) { |
| virtualQuery.setExpression(jpqlQuery); |
| queryHelper.setQuery(virtualQuery); |
| return queryHelper.buildContentAssistProposals(position, buildContentAssistExtension()); |
| } |
| |
| /** |
| * Creates an array of two elements containing the result of calculating the valid proposals. |
| * |
| * @param jpqlQuery The JPQL query to parse and for which content assist proposals will be |
| * calculated based on the position of the cursor within that query |
| * @param position The position of the cursor within the given JPQL query |
| * @param proposals The collection of expected proposals |
| * @return The result based on what was found and what is expected: |
| * <ul> |
| * <li>Index 0: Proposals that were not found by content assist but that are expected;</li> |
| * <li>Index 1: Proposals that were found by content assist but that are not expected.</li> |
| * </ul> |
| */ |
| @SuppressWarnings("unchecked") |
| protected List<String>[] buildResults(String jpqlQuery, |
| int position, |
| Iterable<String> proposals) { |
| |
| // In case the Iterable is read-only |
| List<String> expectedProposals = new ArrayList<>(); |
| CollectionTools.addAll(expectedProposals, proposals); |
| |
| ContentAssistProposals contentAssistProposals = buildContentAssistProposals(jpqlQuery, position); |
| List<String> unexpectedProposals = new ArrayList<>(); |
| |
| // Entities |
| for (IEntity entity : contentAssistProposals.abstractSchemaTypes()) { |
| handleProposal(expectedProposals, unexpectedProposals, entity.getName()); |
| } |
| |
| // Class names |
| for (String className : contentAssistProposals.classNames()) { |
| handleProposal(expectedProposals, unexpectedProposals, className); |
| } |
| |
| // Column names |
| for (String columnName : contentAssistProposals.columnNames()) { |
| handleProposal(expectedProposals, unexpectedProposals, columnName); |
| } |
| |
| // Enum constant names |
| for (EnumProposals enumProposals : contentAssistProposals.enumConstant()) { |
| // Iterate through the list of enum constants |
| for (String enumConstantName : enumProposals.enumConstants()) { |
| handleProposal(expectedProposals, unexpectedProposals, enumConstantName); |
| } |
| } |
| |
| // Identification variables |
| for (String identificationVariable : contentAssistProposals.identificationVariables()) { |
| handleProposal(expectedProposals, unexpectedProposals, identificationVariable); |
| } |
| |
| // JPQL identifiers |
| for (String identifier : contentAssistProposals.identifiers()) { |
| handleProposal(expectedProposals, unexpectedProposals, identifier); |
| } |
| |
| // Mappings |
| for (IMapping mapping : contentAssistProposals.mappings()) { |
| handleProposal(expectedProposals, unexpectedProposals, mapping.getName()); |
| } |
| |
| // Table names |
| for (String tableName : contentAssistProposals.tableNames()) { |
| handleProposal(expectedProposals, unexpectedProposals, tableName); |
| } |
| |
| // The remaining proposals were not part of any proposal list |
| List<String> proposalsNotRemoved = new ArrayList<>(); |
| CollectionTools.addAll(proposalsNotRemoved, expectedProposals); |
| |
| return (List<String>[]) new List[] { proposalsNotRemoved, unexpectedProposals }; |
| } |
| |
| protected List<String> classNames() { |
| List<String> classNames = new ArrayList<>(); |
| classNames.add(Address .class.getName()); |
| classNames.add(ArrayList .class.getName()); |
| classNames.add(Employee .class.getName()); |
| classNames.add(Project .class.getName()); |
| classNames.add(String .class.getName()); |
| classNames.add(StringBuilder.class.getName()); |
| return classNames; |
| } |
| |
| protected final Iterable<String> collectionValuedFieldNames(Class<?> persistentType) throws Exception { |
| return retrieveMappingNames(persistentType, MappingType.COLLECTION_VALUED_FIELD); |
| } |
| |
| protected List<String> columnNames(String tableName) { |
| |
| List<String> columnNames = new ArrayList<>(); |
| |
| if ("EMPLOYEE".equals(tableName)) { |
| columnNames.add("ADDRESS"); |
| columnNames.add("EMPLOYEE_ID"); |
| columnNames.add("FIRST_NAME"); |
| columnNames.add("LAST_NAME"); |
| columnNames.add("MANAGER"); |
| } |
| else if ("ADDRESS".equals(tableName)) { |
| columnNames.add("ADDRESS_ID"); |
| columnNames.add("APT_NUMBER"); |
| columnNames.add("COUNTRY"); |
| columnNames.add("STREET"); |
| columnNames.add("ZIP_CODE"); |
| } |
| |
| return columnNames; |
| } |
| |
| protected Class<?> defaultAcceptableType(String identifier) { |
| |
| if (identifier == ABS || |
| identifier == PLUS || |
| identifier == MINUS || |
| identifier == AVG || |
| identifier == MOD || |
| identifier == SQRT || |
| identifier == SUM) { |
| |
| return Number.class; |
| } |
| |
| if (identifier == CONCAT || |
| identifier == LENGTH || |
| identifier == LOCATE || |
| identifier == LOWER || |
| identifier == SUBSTRING || |
| identifier == TRIM || |
| identifier == UPPER) { |
| |
| return String.class; |
| } |
| |
| return null; |
| } |
| |
| protected final Iterable<IEntity> entities() throws Exception { |
| return getPersistenceUnit().entities(); |
| } |
| |
| protected final Iterable<String> entityNames() throws Exception { |
| List<String> names = new ArrayList<>(); |
| for (IEntity entity : entities()) { |
| names.add(entity.getName()); |
| } |
| return names; |
| } |
| |
| protected List<String> enumConstants() { |
| |
| List<String> names = new ArrayList<>(); |
| |
| for (Enum<EnumType> enumType : EnumType.values()) { |
| names.add(enumType.name()); |
| } |
| |
| return names; |
| } |
| |
| protected List<String> enumTypes() { |
| List<String> classNames = new ArrayList<>(); |
| classNames.add(EnumType .class.getName()); |
| classNames.add(AccessType.class.getName()); |
| return classNames; |
| } |
| |
| protected final Iterable<String> filter(Iterable<String> proposals, String startsWith) { |
| |
| List<String> results = new ArrayList<>(); |
| |
| for (String proposal : proposals) { |
| if (ExpressionTools.startWithIgnoreCase(proposal, startsWith)) { |
| results.add(proposal); |
| } |
| } |
| |
| return results; |
| } |
| |
| protected final Iterable<String> filter(String[] proposals, String startsWith) { |
| return filter(Arrays.asList(proposals), startsWith); |
| } |
| |
| protected final JPQLGrammar getGrammar() { |
| return queryHelper.getGrammar(); |
| } |
| |
| protected final JPQLQueryBNFAccessor getQueryBNFAccessor() { |
| return bnfAccessor; |
| } |
| |
| protected final AbstractJPQLQueryHelper getQueryHelper() { |
| return queryHelper; |
| } |
| |
| protected final JavaQuery getVirtualQuery() { |
| return virtualQuery; |
| } |
| |
| private void handleProposal(Iterable<String> expectedProposals, |
| List<String> unexpectedProposals, |
| String possibleProposal) { |
| |
| boolean removed = false; |
| |
| // Iterate through the expected proposals and see if the class name is expected |
| for (Iterator<String> iter = expectedProposals.iterator(); iter.hasNext(); ) { |
| |
| String expectedProposal = iter.next(); |
| |
| // The proposal is an expected proposal |
| if (expectedProposal.equalsIgnoreCase(possibleProposal)) { |
| // Remove it from the list of expected proposals |
| iter.remove(); |
| // Indicate it was removed, if not, we'll add the proposal |
| // to the list of unexpected proposals |
| removed = true; |
| break; |
| } |
| } |
| |
| // The proposal found by content assist is not expected |
| if (!removed) { |
| unexpectedProposals.add(possibleProposal); |
| } |
| } |
| |
| protected final boolean isJPA1_0() { |
| return jpqlGrammar().getJPAVersion() == JPAVersion.VERSION_1_0; |
| } |
| |
| protected final boolean isJPA2_0() { |
| return jpqlGrammar().getJPAVersion() == JPAVersion.VERSION_2_0; |
| } |
| |
| protected final boolean isJPA2_1() { |
| return jpqlGrammar().getJPAVersion() == JPAVersion.VERSION_2_1; |
| } |
| |
| protected final List<String> joinIdentifiers() { |
| List<String> proposals = new ArrayList<>(); |
| proposals.add(INNER_JOIN); |
| proposals.add(INNER_JOIN_FETCH); |
| proposals.add(JOIN); |
| proposals.add(JOIN_FETCH); |
| proposals.add(LEFT_JOIN); |
| proposals.add(LEFT_JOIN_FETCH); |
| proposals.add(LEFT_OUTER_JOIN); |
| proposals.add(LEFT_OUTER_JOIN_FETCH); |
| return proposals; |
| } |
| |
| protected final List<String> joinOnlyIdentifiers() { |
| List<String> proposals = new ArrayList<>(); |
| proposals.add(INNER_JOIN); |
| proposals.add(JOIN); |
| proposals.add(LEFT_JOIN); |
| proposals.add(LEFT_OUTER_JOIN); |
| return proposals; |
| } |
| |
| protected final JPQLGrammar jpqlGrammar() { |
| return queryHelper.getGrammar(); |
| } |
| |
| protected final Iterable<String> nonTransientFieldNames(Class<?> persistentType) throws Exception { |
| return retrieveMappingNames(persistentType, MappingType.NON_TRANSIENT); |
| } |
| |
| protected final Iterable<String> relationshipAndCollectionFieldNames(Class<?> persistentType) throws Exception { |
| Set<String> uniqueNames = new HashSet<>(); |
| CollectionTools.addAll(uniqueNames, relationshipFieldNames(persistentType)); |
| CollectionTools.addAll(uniqueNames, collectionValuedFieldNames(persistentType)); |
| return uniqueNames; |
| } |
| |
| protected final Iterable<String> relationshipFieldNames(Class<?> persistentType) throws Exception { |
| return retrieveMappingNames(persistentType, MappingType.RELATIONSHIP_FIELD); |
| } |
| |
| protected final <T extends Collection<String>> T removeAll(T items1, Iterable<String> items2) { |
| for (String item2 : items2) { |
| items1.remove(item2); |
| } |
| return items1; |
| } |
| |
| protected final Iterable<String> retrieveMappingNames(Class<?> persistentType, |
| MappingType mappingType) throws Exception { |
| |
| return retrieveMappingNames(persistentType, mappingType, null); |
| } |
| |
| protected final Iterable<String> retrieveMappingNames(Class<?> persistentType, |
| MappingType mappingType, |
| Class<?> allowedType) throws Exception { |
| |
| IManagedType managedType = getPersistenceUnit().getManagedType(persistentType.getName()); |
| |
| if (managedType == null) { |
| return Collections.emptyList(); |
| } |
| |
| List<String> names = new ArrayList<>(); |
| IType type = (allowedType != null) ? getPersistenceUnit().getTypeRepository().getType(allowedType) : null; |
| |
| for (IMapping mapping : managedType.mappings()) { |
| |
| if (mappingType.isValid(mapping) && |
| ((type == null) || mapping.getType().isAssignableTo(type))) { |
| |
| names.add(mapping.getName()); |
| } |
| // Allow incomplete path |
| else if (mappingType == MappingType.SINGLE_VALUED_OBJECT_FIELD) { |
| if (mapping.isRelationship() && !mapping.isCollection()) { |
| names.add(mapping.getName()); |
| } |
| } |
| } |
| |
| return names; |
| } |
| |
| @Override |
| protected void setUpClass() throws Exception { |
| super.setUpClass(); |
| virtualQuery = new JavaQuery(getPersistenceUnit(), null); |
| bnfAccessor = new JPQLQueryBNFAccessor(getGrammar().getExpressionRegistry()); |
| } |
| |
| protected final Iterable<String> singledValuedObjectFieldNames(Class<?> persistentType) throws Exception { |
| return retrieveMappingNames(persistentType, MappingType.SINGLE_VALUED_OBJECT_FIELD); |
| } |
| |
| protected final Iterable<String> singledValuedObjectFieldNames(Class<?> persistentType, |
| Class<?> allowedType) throws Exception { |
| |
| return retrieveMappingNames(persistentType, MappingType.SINGLE_VALUED_OBJECT_FIELD, allowedType); |
| } |
| |
| protected final Iterable<String> stateFieldNames(Class<?> persistentType) throws Exception { |
| return retrieveMappingNames(persistentType, MappingType.STATE_FIELD); |
| } |
| |
| protected final Iterable<String> stateFieldNames(Class<?> persistentType, Class<?> allowedType) throws Exception { |
| return retrieveMappingNames(persistentType, MappingType.STATE_FIELD, allowedType); |
| } |
| |
| protected List<String> tableNames() { |
| |
| List<String> tableNames = new ArrayList<>(); |
| tableNames.add("ADDRESS"); |
| tableNames.add("EMPLOYEE"); |
| tableNames.add("EMPLOYEE_SEQ"); |
| tableNames.add("MANAGER"); |
| tableNames.add("DEPARTMENT"); |
| |
| return tableNames; |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| queryHelper.dispose(); |
| virtualQuery.setExpression(null); |
| super.tearDown(); |
| } |
| |
| @Override |
| protected void tearDownClass() throws Exception { |
| bnfAccessor = null; |
| queryHelper = null; |
| virtualQuery = null; |
| super.tearDownClass(); |
| } |
| |
| protected final void testDoesNotHaveTheseProposals(String jpqlQuery, |
| int position, |
| Enum<?>... proposals) { |
| |
| testDoesNotHaveTheseProposals(jpqlQuery, position, toString(proposals)); |
| } |
| |
| protected final void testDoesNotHaveTheseProposals(String jpqlQuery, |
| int position, |
| Iterable<String> proposals) { |
| |
| List<String>[] results = buildResults(jpqlQuery, position, Collections.<String>emptyList()); |
| List<String> expectedEroposals = results[1]; |
| List<String> unexpectedProposals = new ArrayList<>(); |
| |
| for (String proposal : proposals) { |
| if (expectedEroposals.remove(proposal)) { |
| unexpectedProposals.add(proposal); |
| } |
| } |
| |
| assertTrue(unexpectedProposals + " should not be proposals.", unexpectedProposals.isEmpty()); |
| } |
| |
| protected final void testDoesNotHaveTheseProposals(String jpqlQuery, |
| int position, |
| String... proposals) { |
| |
| testDoesNotHaveTheseProposals(jpqlQuery, position, CollectionTools.list(proposals)); |
| } |
| |
| protected final void testHasNoProposals(String jpqlQuery, int position) { |
| List<String>[] results = buildResults(jpqlQuery, position, Collections.<String>emptyList()); |
| List<String> unexpectedProposals = results[1]; |
| assertTrue(unexpectedProposals + " should not be proposals.", unexpectedProposals.isEmpty()); |
| } |
| |
| protected final void testHasOnlyTheseProposals(String jpqlQuery, |
| int position, |
| Enum<?>... proposals) { |
| |
| testHasOnlyTheseProposals(jpqlQuery, position, toString(proposals)); |
| } |
| |
| protected final void testHasOnlyTheseProposals(String jpqlQuery, int position, Iterable<String> proposals) { |
| |
| List<String>[] results = buildResults(jpqlQuery, position, proposals); |
| List<String> proposalsNotRemoved = results[0]; |
| List<String> unexpectedProposals = results[1]; |
| |
| // Inconsistent list of proposals |
| if (!unexpectedProposals.isEmpty() && !proposalsNotRemoved.isEmpty()) { |
| if (proposalsNotRemoved.size() == 1) { |
| fail(proposalsNotRemoved.get(0) + " should be a proposal and " + unexpectedProposals + " should not be a proposal."); |
| } |
| else { |
| fail(proposalsNotRemoved + " should be proposals and " + unexpectedProposals + " should not be proposals."); |
| } |
| } |
| // Added more proposals than it should |
| else if (!unexpectedProposals.isEmpty() && proposalsNotRemoved.isEmpty()) { |
| if (proposalsNotRemoved.size() == 1) { |
| fail(unexpectedProposals.get(0) + " should not be a proposal."); |
| } |
| else { |
| fail(unexpectedProposals + " should not be proposals."); |
| } |
| } |
| // Forgot to add some proposals |
| else if (!proposalsNotRemoved.isEmpty()) { |
| if (proposalsNotRemoved.size() == 1) { |
| fail(proposalsNotRemoved.get(0) + " should be a proposal."); |
| } |
| else { |
| fail(proposalsNotRemoved + " should be proposals."); |
| } |
| } |
| } |
| |
| protected final void testHasOnlyTheseProposals(String jpqlQuery, |
| int position, |
| String... proposals) { |
| |
| if (proposals.length == 0) { |
| fail("The list of expected proposals cannot be empty"); |
| } |
| |
| testHasOnlyTheseProposals(jpqlQuery, position, CollectionTools.list(proposals)); |
| } |
| |
| protected final void testHasTheseProposals(String jpqlQuery, |
| int position, |
| Enum<?>... enums) { |
| |
| testHasTheseProposals(jpqlQuery, position, toString(enums)); |
| } |
| |
| protected final void testHasTheseProposals(String jpqlQuery, |
| int position, |
| Iterable<String> proposals) { |
| |
| List<String>[] results = buildResults(jpqlQuery, position, proposals); |
| List<String> proposalsNotRemoved = results[0]; |
| assertTrue(proposalsNotRemoved + " should be proposals.", proposalsNotRemoved.isEmpty()); |
| } |
| |
| protected final void testHasTheseProposals(String jpqlQuery, |
| int position, |
| String... proposals) { |
| |
| testHasTheseProposals(jpqlQuery, position, CollectionTools.list(proposals)); |
| } |
| |
| protected final String[] toString(Enum<?>[] enums) { |
| |
| String[] names = new String[enums.length]; |
| |
| for (int index = enums.length; --index >= 0; ) { |
| names[index] = enums[index].name(); |
| } |
| |
| return names; |
| } |
| |
| protected final Iterable<String> transientFieldNames(Class<?> persistentType) throws Exception { |
| return retrieveMappingNames(persistentType, MappingType.TRANSIENT); |
| } |
| |
| private enum MappingType { |
| |
| COLLECTION_VALUED_FIELD { |
| @Override |
| public boolean isValid(IMapping mapping) { |
| return mapping.isCollection(); |
| } |
| }, |
| |
| NON_TRANSIENT { |
| @Override |
| public boolean isValid(IMapping mapping) { |
| return !mapping.isTransient(); |
| } |
| }, |
| |
| RELATIONSHIP_FIELD { |
| @Override |
| public boolean isValid(IMapping mapping) { |
| return mapping.isRelationship() && !mapping.isCollection(); |
| } |
| }, |
| |
| SINGLE_VALUED_OBJECT_FIELD { |
| @Override |
| public boolean isValid(IMapping mapping) { |
| return !mapping.isTransient() && !mapping.isCollection(); |
| } |
| }, |
| |
| STATE_FIELD { |
| @Override |
| public boolean isValid(IMapping mapping) { |
| return mapping.isProperty(); |
| } |
| }, |
| |
| TRANSIENT { |
| @Override |
| public boolean isValid(IMapping mapping) { |
| return mapping.isTransient(); |
| } |
| }; |
| |
| abstract boolean isValid(IMapping mapping); |
| } |
| } |