blob: 8d9377d5b5499421f04e9cf887c75f61c22cbb43 [file] [log] [blame]
/*
* 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);
@SuppressWarnings({"rawtypes"})
List<String>[] list = (List<String>[]) new List[] { proposalsNotRemoved, unexpectedProposals };
return list;
}
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);
}
}