| /* |
| * Copyright (c) 2006, 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.jpql.tools; |
| |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.BETWEEN; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.DELETE_FROM; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.IN; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.INNER_JOIN; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.INNER_JOIN_FETCH; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.IS_EMPTY; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.IS_NOT_EMPTY; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.IS_NOT_NULL; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.IS_NULL; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.JOIN; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.JOIN_FETCH; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.LEFT_JOIN; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.LEFT_JOIN_FETCH; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.LEFT_OUTER_JOIN; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.LEFT_OUTER_JOIN_FETCH; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.MEMBER; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.MEMBER_OF; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.NOT_BETWEEN; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.NOT_IN; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.NOT_MEMBER; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.NOT_MEMBER_OF; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.SELECT; |
| import static org.eclipse.persistence.jpa.jpql.parser.Expression.UPDATE; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.persistence.jpa.jpql.ExpressionTools; |
| import org.eclipse.persistence.jpa.jpql.WordParser; |
| import org.eclipse.persistence.jpa.jpql.parser.IdentifierRole; |
| import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IEntity; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IMapping; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IType; |
| import org.eclipse.persistence.jpa.jpql.tools.utility.XmlEscapeCharacterConverter; |
| import org.eclipse.persistence.jpa.jpql.tools.utility.iterable.SnapshotCloneIterable; |
| |
| /** |
| * The default implementation of {@link ContentAssistProposals} which stores the valid proposals. |
| * <p> |
| * Provisional API: This interface is part of an interim API that is still under development and |
| * expected to change significantly before reaching stability. It is available at this early stage |
| * to solicit feedback from pioneering adopters on the understanding that any code that uses this |
| * API will almost certainly be broken (repeatedly) as the API evolves. |
| * |
| * @version 2.5 |
| * @since 2.3 |
| * @author Pascal Filion |
| */ |
| @SuppressWarnings("nls") |
| public final class DefaultContentAssistProposals implements ContentAssistProposals { |
| |
| /** |
| * The prefix that is used to filter the list of class names. |
| * |
| * @since 2.5 |
| */ |
| private String classNamePrefix; |
| |
| /** |
| * The class type helps to filter the various types of classes, i.e. anonymous, member, interface |
| * are never allowed. |
| * |
| * @since 2.5 |
| */ |
| private ClassType classType; |
| |
| /** |
| * TODO |
| * |
| * @since 2.5 |
| */ |
| private String columnNamePrefix; |
| |
| /** |
| * The set of possible abstract schema types. |
| */ |
| private Set<IEntity> entities; |
| |
| /** |
| * TODO |
| * |
| * @since 2.5 |
| */ |
| private Map<IType, DefaultEnumProposals> enumProposals; |
| |
| /** |
| * This extension can be used to provide additional support to JPQL content assist that is |
| * outside the scope of providing proposals related to JPA metadata. It adds support for |
| * providing suggestions related to class names, enum constants, table names, column names. |
| * |
| * @since 2.5 |
| */ |
| private ContentAssistExtension extension; |
| |
| /** |
| * The set of possible identification variables. |
| */ |
| private Set<String> identificationVariables; |
| |
| /** |
| * The set of possible JPQL identifiers. |
| */ |
| private Set<String> identifiers; |
| |
| /** |
| * The {@link JPQLGrammar} that defines how the JPQL query was parsed. |
| */ |
| private JPQLGrammar jpqlGrammar; |
| |
| /** |
| * The set of possible {@link IMapping mappings}, which can be state fields, association fields |
| * and/or collection fields. |
| */ |
| private Set<IMapping> mappings; |
| |
| /** |
| * The identification variables mapped to their abstract schema types. |
| */ |
| private Map<String, IEntity> rangeIdentificationVariables; |
| |
| /** |
| * TODO |
| * |
| * @since 2.5 |
| */ |
| private String tableName; |
| |
| /** |
| * TODO |
| * |
| * @since 2.5 |
| */ |
| private String tableNamePrefix; |
| |
| /** |
| * A JPQL identifier that is mapped to its longest counterpart. |
| */ |
| private static final Map<String, String> LONGUEST_IDENTIFIERS = buildLonguestIdentifiers(); |
| |
| /** |
| * A JPQL identifier that is mapped to the list of counterparts used to find them in the query. |
| */ |
| private static final Map<String, List<String>> ORDERED_IDENTIFIERS = buildOrderedIdentifiers(); |
| |
| /** |
| * Creates a new <code>DefaultContentAssistProposals</code>. |
| * |
| * @param jpqlGrammar The {@link JPQLGrammar} that defines how the JPQL query was parsed |
| * @param extension This extension can be used to provide additional support to JPQL content |
| * assist that is outside the scope of providing proposals related to JPA metadata. It adds |
| * support for providing suggestions related to class names, enum constants, table names, column |
| * names |
| */ |
| public DefaultContentAssistProposals(JPQLGrammar jpqlGrammar, ContentAssistExtension extension) { |
| super(); |
| initialize(jpqlGrammar, extension); |
| } |
| |
| private static Map<String, String> buildLonguestIdentifiers() { |
| |
| Map<String, String> identifiers = new HashMap<String, String>(); |
| |
| identifiers.put(IS_EMPTY, IS_NOT_EMPTY); |
| identifiers.put(IS_NULL, IS_NOT_NULL); |
| identifiers.put(IN, NOT_IN); |
| identifiers.put(BETWEEN, NOT_BETWEEN); |
| identifiers.put(MEMBER, NOT_MEMBER_OF); |
| identifiers.put(MEMBER_OF, NOT_MEMBER_OF); |
| identifiers.put(NOT_MEMBER, NOT_MEMBER_OF); |
| identifiers.put(JOIN, LEFT_OUTER_JOIN_FETCH); |
| identifiers.put(JOIN_FETCH, LEFT_OUTER_JOIN_FETCH); |
| identifiers.put(LEFT_JOIN, LEFT_OUTER_JOIN_FETCH); |
| identifiers.put(LEFT_JOIN_FETCH, LEFT_OUTER_JOIN_FETCH); |
| identifiers.put(LEFT_OUTER_JOIN, LEFT_OUTER_JOIN_FETCH); |
| identifiers.put(INNER_JOIN, LEFT_OUTER_JOIN_FETCH); |
| identifiers.put(INNER_JOIN_FETCH, LEFT_OUTER_JOIN_FETCH); |
| |
| identifiers.put(SELECT, DELETE_FROM); |
| identifiers.put(UPDATE, DELETE_FROM); |
| |
| return identifiers; |
| } |
| |
| private static Map<String, List<String>> buildOrderedIdentifiers() { |
| |
| Map<String, List<String>> identifiers = new HashMap<String, List<String>>(); |
| |
| identifiers.put(IS_NOT_EMPTY, Collections.singletonList(IS_NOT_EMPTY)); |
| identifiers.put(IS_NOT_NULL, Collections.singletonList(IS_NOT_NULL)); |
| identifiers.put(NOT_IN, Collections.singletonList(NOT_IN)); |
| identifiers.put(NOT_BETWEEN, Collections.singletonList(NOT_BETWEEN)); |
| |
| List<String> members = new ArrayList<String>(); |
| members.add(MEMBER_OF); |
| members.add(NOT_MEMBER); |
| members.add(MEMBER); |
| identifiers.put(NOT_MEMBER_OF, members); |
| |
| List<String> joins = new ArrayList<String>(); |
| joins.add(LEFT_OUTER_JOIN); |
| joins.add(LEFT_JOIN_FETCH); |
| joins.add(LEFT_JOIN); |
| joins.add(INNER_JOIN_FETCH); |
| joins.add(INNER_JOIN); |
| joins.add(JOIN_FETCH); |
| joins.add(JOIN); |
| identifiers.put(LEFT_OUTER_JOIN_FETCH, joins); |
| |
| List<String> clauses = new ArrayList<String>(); |
| clauses.add(SELECT); |
| clauses.add(UPDATE); |
| identifiers.put(DELETE_FROM, clauses); |
| |
| return identifiers; |
| } |
| |
| @Override |
| public Iterable<IEntity> abstractSchemaTypes() { |
| return new SnapshotCloneIterable<IEntity>(entities); |
| } |
| |
| /** |
| * Adds the given {@link IEntity} as a possible abstract schema type. |
| * |
| * @param abstractSchemaType The abstract schema type that is a valid proposal |
| */ |
| public void addEntity(IEntity abstractSchemaType) { |
| entities.add(abstractSchemaType); |
| } |
| |
| /** |
| * Adds the constants of the given enum constant as a valid proposal. |
| * |
| * @param enumType The {@link IType} of the enum type |
| * @param enumConstant The enum constant to be added as a valid proposal |
| * |
| * @since 2.5 |
| */ |
| public void addEnumConstant(IType enumType, String enumConstant) { |
| |
| DefaultEnumProposals proposal = enumProposals.get(enumType); |
| |
| if (proposal == null) { |
| proposal = new DefaultEnumProposals(enumType); |
| enumProposals.put(enumType, proposal); |
| } |
| |
| proposal.constants.add(enumConstant); |
| } |
| |
| /** |
| * Adds the given identification variable as a proposal. |
| * |
| * @param identificationVariable The identification variable that is a valid proposal |
| */ |
| public void addIdentificationVariable(String identificationVariable) { |
| identificationVariables.add(identificationVariable); |
| } |
| |
| /** |
| * Adds the given JPQL identifier as a proposal. |
| * |
| * @param identifier The JPQL identifier that is a valid proposal |
| */ |
| public void addIdentifier(String identifier) { |
| identifiers.add(identifier); |
| } |
| |
| /** |
| * Adds the given {@link IMapping mapping} (state field, association field or collection field) |
| * as a valid proposal. |
| * |
| * @param mapping The {@link IMapping} of the state field, association field or collection field |
| */ |
| public void addMapping(IMapping mapping) { |
| mappings.add(mapping); |
| } |
| |
| /** |
| * Adds the given {@link IMapping mappings} (state fields, association fields or collection fields) |
| * as valid proposals. |
| * |
| * @param mappings The {@link IMapping mappings} of the state fields, association fields or |
| * collection fields |
| */ |
| public void addMappings(Collection<IMapping> mappings) { |
| this.mappings.addAll(mappings); |
| } |
| |
| /** |
| * Adds the given range identification variable that is mapping the given abstract schema type. |
| * |
| * @param identificationVariable The range identification variable mapping the abstract schema name |
| * @param entity The abstract type name that identifies the type of the variable |
| */ |
| public void addRangeIdentificationVariable(String identificationVariable, IEntity entity) { |
| rangeIdentificationVariables.put(identificationVariable, entity); |
| } |
| |
| @Override |
| public Result buildEscapedQuery(String jpqlQuery, String proposal, int position, boolean insert) { |
| |
| Result result = buildQuery(jpqlQuery, proposal, position, insert); |
| |
| // Escape the JPQL query and adjust the position accordingly |
| int[] positions = { result.position }; |
| result.jpqlQuery = ExpressionTools.escape(result.jpqlQuery, positions); |
| result.position = positions[0]; |
| |
| return result; |
| } |
| |
| /** |
| * Calculates the start and end position for correctly inserting the proposal into the query. |
| * |
| * @param wordParser This parser can be used to retrieve words from the cursor position |
| * @param proposal The proposal to be inserted into the query |
| * @param insert Flag that determines if the proposal is simply inserted or it should also replace |
| * the partial word following the position of the cursor |
| * @return The start and end positions |
| */ |
| public int[] buildPositions(WordParser wordParser, String proposal, boolean insert) { |
| |
| int index; |
| String wordsToReplace = null; |
| |
| // Special case for arithmetic symbol (<, <=, =, <>, >, >=, +, -, *, /) |
| if (wordParser.isArithmeticSymbol(proposal.charAt(0))) { |
| int startIndex = wordParser.position(); |
| int endIndex = startIndex; |
| char character; |
| |
| // Now look for the start index |
| do { |
| character = wordParser.character(--startIndex); |
| } |
| while (wordParser.isArithmeticSymbol(character)); |
| |
| // Now look for the end index |
| do { |
| character = wordParser.character(endIndex++); |
| } |
| while (wordParser.isArithmeticSymbol(character)); |
| |
| // Increment the start index because the loop always decrements it by one before stopping |
| // and decrement the end position because it's always incremented by one before stopping |
| index = ++startIndex; |
| wordsToReplace = wordParser.substring(index, --endIndex); |
| } |
| else { |
| // Always try to find the start position using the longest JPQL identifier |
| // if the proposal has more than one word |
| wordsToReplace = longuestIdentifier(proposal); |
| index = startPosition(wordParser, wordsToReplace); |
| |
| // Now check with the other possibilities |
| if ((index == -1) && ORDERED_IDENTIFIERS.containsKey(wordsToReplace)) { |
| for (String identifier : ORDERED_IDENTIFIERS.get(wordsToReplace)) { |
| wordsToReplace = identifier; |
| index = startPosition(wordParser, wordsToReplace); |
| if (index > -1) { |
| break; |
| } |
| } |
| } |
| } |
| |
| // Now calculate the start position |
| int startPosition; |
| |
| if (index > -1) { |
| startPosition = index; |
| } |
| else { |
| // Check for path expression (which uses a dot) and update the wordToReplace |
| // and startPosition accordingly |
| String partialWord = wordParser.partialWord(); |
| String entireWord = wordParser.entireWord(); |
| int dotIndex = -1; |
| |
| if (isMappingName(proposal) || |
| isEnumConstant(proposal) || |
| isColumnName(proposal)) { |
| |
| dotIndex = partialWord.lastIndexOf('.'); |
| } |
| |
| if (dotIndex > 0) { |
| wordsToReplace = entireWord.substring(dotIndex + 1); |
| startPosition = wordParser.position() - partialWord.length() + dotIndex + 1; |
| } |
| else if (insert) { |
| wordsToReplace = wordParser.word(); |
| startPosition = wordParser.position() - partialWord.length(); |
| } |
| else { |
| wordsToReplace = entireWord; |
| startPosition = wordParser.position() - partialWord.length(); |
| } |
| } |
| |
| // Now calculate the end position |
| int length; |
| |
| // Only the partial word to the left of the cursor will be replaced and |
| // the partial word to the right of the cursor will stay |
| if (insert) { |
| String partialWord = wordParser.substring(startPosition, wordParser.position()); |
| length = partialWord.length(); |
| } |
| else if (proposal != wordsToReplace) { |
| length = wordsToReplace.length(); |
| } |
| else { |
| index = 0; |
| |
| for (int charIndex = 0, wordLength = wordParser.length(), count = wordsToReplace.length(); |
| startPosition + index < wordLength && charIndex < count; |
| index++, charIndex++) { |
| |
| // Find the end position by matching the proposal with the content of the query by |
| // scanning every character starting at the start position, the end position will be |
| // the position of the first character that is different or at the end of the query |
| if (wordParser.character(startPosition + charIndex) != |
| wordsToReplace.charAt(charIndex)) { |
| |
| break; |
| } |
| } |
| |
| length = index; |
| } |
| |
| return new int[] { startPosition, startPosition + length }; |
| } |
| |
| @Override |
| public Result buildQuery(String jpqlQuery, String proposal, int position, boolean insert) { |
| |
| // Nothing to replace |
| if (ExpressionTools.stringIsEmpty(proposal)) { |
| return new Result(jpqlQuery, position); |
| } |
| |
| WordParser wordParser = new WordParser(jpqlQuery); |
| wordParser.setPosition(position); |
| |
| // Calculate the start and end positions |
| int[] positions = buildPositions(wordParser, proposal, insert); |
| |
| // Create the new query |
| StringBuilder sb = new StringBuilder(jpqlQuery); |
| sb.replace(positions[0], positions[1], proposal); |
| |
| // Return the result |
| return new Result(sb.toString(), positions[0] + proposal.length()); |
| } |
| |
| @Override |
| public ResultQuery buildXmlQuery(String jpqlQuery, String proposal, int position, boolean insert) { |
| |
| // Nothing to replace |
| if (ExpressionTools.stringIsEmpty(proposal)) { |
| return new Result(jpqlQuery, position); |
| } |
| |
| int[] positions = { position }; |
| |
| // First convert the escape characters into their unicode characters |
| String query = XmlEscapeCharacterConverter.unescape(jpqlQuery, positions); |
| |
| // Calculate the start and end positions |
| WordParser wordParser = new WordParser(query); |
| wordParser.setPosition(positions[0]); |
| int[] proposalPositions = buildPositions(wordParser, proposal, insert); |
| |
| // Escape the proposal |
| proposal = XmlEscapeCharacterConverter.escape(proposal, new int[1]); |
| |
| // Adjust the positions so it's in the original JPQL query, which may contain escaped characters |
| XmlEscapeCharacterConverter.reposition(jpqlQuery, proposalPositions); |
| |
| // Create the new JPQL query |
| StringBuilder sb = new StringBuilder(jpqlQuery); |
| sb.replace(proposalPositions[0], proposalPositions[1], proposal); |
| |
| // Return the result |
| return new Result(sb.toString(), proposalPositions[0] + proposal.length()); |
| } |
| |
| @Override |
| public Iterable<String> classNames() { |
| if (classNamePrefix == null) { |
| return Collections.emptyList(); |
| } |
| return extension.classNames(classNamePrefix, classType); |
| } |
| |
| @Override |
| public Iterable<String> columnNames() { |
| if (tableName == null) { |
| return Collections.emptyList(); |
| } |
| return extension.columnNames(tableName, columnNamePrefix); |
| } |
| |
| @Override |
| public Iterable<EnumProposals> enumConstant() { |
| return new SnapshotCloneIterable<EnumProposals>(enumProposals.values()); |
| } |
| |
| @Override |
| public IEntity getAbstractSchemaType(String identificationVariable) { |
| return rangeIdentificationVariables.get(identificationVariable); |
| } |
| |
| /** |
| * Returns the prefix that will be used to filter the list of possible class names. |
| * |
| * @return The prefix that is used to filter the list of class names or <code>null</code> if it |
| * was not set for the cursor position within the JPQL query |
| * @since 2.5 |
| */ |
| public String getClassNamePrefix() { |
| return classNamePrefix; |
| } |
| |
| @Override |
| public ClassType getClassType() { |
| return classType; |
| } |
| |
| /** |
| * Returns the prefix that will be used by {@link ContentAssistExtension} to filter the column |
| * names if the table name is not <code>null</code>. |
| * |
| * @return The prefix that is used to filter the list of columns names, which is <code>null</code> |
| * if it has not been set along with the table name |
| * @since 2.5 |
| */ |
| public String getColumnNamePrefix() { |
| return columnNamePrefix; |
| } |
| |
| /** |
| * Returns the {@link JPQLGrammar} that defines how the JPQL query was parsed. |
| * |
| * @return The {@link JPQLGrammar} that was used to parse this {@link org.eclipse.persistence.jpa.jpql.parser.Expression Expression} |
| */ |
| public JPQLGrammar getGrammar() { |
| return jpqlGrammar; |
| } |
| |
| @Override |
| public IdentifierRole getIdentifierRole(String identifier) { |
| return jpqlGrammar.getExpressionRegistry().getIdentifierRole(identifier); |
| } |
| |
| /** |
| * Returns the table name that will be used by {@link ContentAssistExtension} to retrieve the |
| * column names. |
| * |
| * @return The name of the table for which its column names should be retrieve as possible proposals |
| * @since 2.5 |
| */ |
| public String getTableName() { |
| return tableName; |
| } |
| |
| /** |
| * Returns the prefix that will be used to filter the list of possible table names. |
| * |
| * @return The prefix that is used to filter the list of table names or <code>null</code> if it |
| * was not set for the cursor position within the JPQL query |
| * @since 2.5 |
| */ |
| public String getTableNamePrefix() { |
| return tableNamePrefix; |
| } |
| |
| @Override |
| public boolean hasProposals() { |
| return !mappings.isEmpty() || |
| !entities.isEmpty() || |
| !identifiers.isEmpty() || |
| !enumProposals.isEmpty() || |
| !identificationVariables.isEmpty() || |
| classNames().iterator().hasNext() || |
| tableNames().iterator().hasNext() || |
| columnNames().iterator().hasNext() || |
| !rangeIdentificationVariables.isEmpty(); |
| } |
| |
| @Override |
| public Iterable<String> identificationVariables() { |
| List<String> variables = new ArrayList<String>(identificationVariables.size() + rangeIdentificationVariables.size()); |
| variables.addAll(identificationVariables); |
| variables.addAll(rangeIdentificationVariables.keySet()); |
| return new SnapshotCloneIterable<String>(variables); |
| } |
| |
| @Override |
| public Iterable<String> identifiers() { |
| return new SnapshotCloneIterable<String>(identifiers); |
| } |
| |
| protected void initialize(JPQLGrammar jpqlGrammar, ContentAssistExtension extension) { |
| |
| this.extension = extension; |
| this.jpqlGrammar = jpqlGrammar; |
| this.mappings = new HashSet<IMapping>(); |
| this.identifiers = new HashSet<String>(); |
| this.entities = new HashSet<IEntity>(); |
| this.identificationVariables = new HashSet<String>(); |
| this.rangeIdentificationVariables = new HashMap<String, IEntity>(); |
| this.enumProposals = new HashMap<IType, DefaultEnumProposals>(); |
| } |
| |
| |
| /** |
| * Determines whether the given proposal is a column name (which should be unqualified). |
| * |
| * @param proposal The proposal that is being inserted into the JPQL query |
| * @return <code>true</code> if the given proposal is a column name; <code>false</code> otherwise |
| * @since 2.5 |
| */ |
| public boolean isColumnName(String proposal) { |
| |
| for (String columName : columnNames()) { |
| if (columName.equals(proposal)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Determines whether the given proposal is an enum constant name (which should be unqualified). |
| * |
| * @param proposal The proposal that is being inserted into the JPQL query |
| * @return <code>true</code> if the given proposal is a unqualified enum constant name; |
| * <code>false</code> otherwise |
| * @since 2.5 |
| */ |
| public boolean isEnumConstant(String proposal) { |
| |
| for (EnumProposals enumProposal : enumProposals.values()) { |
| for (String enumConstant : enumProposal.enumConstants()) { |
| if (enumConstant.equals(proposal)) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Determines whether the given proposal is a mapping name. |
| * |
| * @param proposal The proposal that is being inserted into the JPQL query |
| * @return <code>true</code> if the given proposal is a mapping name; <code>false</code> otherwise |
| * @since 2.5 |
| */ |
| public boolean isMappingName(String proposal) { |
| |
| for (IMapping mapping : mappings) { |
| if (mapping.getName().equals(proposal)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns the longest possible JPQL identifier that is related to the given proposal if the |
| * proposal is a JPQL identifier and contains multiple words. For instance, the longest form of |
| * <code>JOIN</code> or <code>JOIN FETCH</code> is <code>LEFT OUTER JOIN FETCH</code>. |
| * |
| * @param proposal The proposal to retrieve its longest form if one is associated with it |
| * @return Either the given proposal if it's not a JPQL identifier or it does not have a longer |
| * form or the longest version of the JPQL identifier |
| */ |
| public String longuestIdentifier(String proposal) { |
| return LONGUEST_IDENTIFIERS.containsKey(proposal) ? LONGUEST_IDENTIFIERS.get(proposal) : proposal; |
| } |
| |
| @Override |
| public Iterable<IMapping> mappings() { |
| return new SnapshotCloneIterable<IMapping>(mappings); |
| } |
| |
| /** |
| * Removes the given JPQL identifier. |
| * |
| * @param identifier The identifier that was added but actually needs to be removed |
| */ |
| protected void removeIdentifier(String identifier) { |
| identifiers.remove(identifier); |
| } |
| |
| /** |
| * Adds the given prefix that will be used to filter the list of possible class names. |
| * |
| * @param prefix The prefix that is used to filter the list of class names |
| * @param classType Determines how to filter the various types of classes |
| * @since 2.5 |
| */ |
| public void setClassNamePrefix(String prefix, ClassType classType) { |
| this.classNamePrefix = prefix; |
| this.classType = classType; |
| } |
| |
| /** |
| * Sets the table name and a prefix that will be used to filter the names of the table's columns. |
| * |
| * @param tableName The name of the table for which its column names should be retrieve as |
| * possible proposals |
| * @param prefix The prefix that is used to filter the list of columns names, which is never |
| * <code>null</code> but can be an empty string |
| * @since 2.5 |
| */ |
| public void setTableName(String tableName, String prefix) { |
| this.tableName = tableName; |
| this.columnNamePrefix = prefix; |
| } |
| |
| /** |
| * Adds the given prefix that will be used to filter the list of possible columns names. |
| * |
| * @param tableNamePrefix The prefix that is used to filter the list of columns names |
| * @since 2.5 |
| */ |
| public void setTableNamePrefix(String tableNamePrefix) { |
| this.tableNamePrefix = tableNamePrefix; |
| } |
| |
| public int startPosition(WordParser wordParser, String proposal) { |
| |
| int index = wordParser.position(); |
| int maxMoveLength = proposal.length(); |
| |
| while (index >= 0 && maxMoveLength > 0) { |
| if (wordParser.startsWithIgnoreCase(proposal, index)) { |
| return index; |
| } |
| index--; |
| maxMoveLength--; |
| } |
| |
| return -1; |
| } |
| |
| @Override |
| public Iterable<String> tableNames() { |
| if (tableNamePrefix == null) { |
| return Collections.emptyList(); |
| } |
| return extension.tableNames(tableNamePrefix); |
| } |
| |
| @Override |
| public String toString() { |
| |
| StringBuilder sb = new StringBuilder(); |
| |
| // JPQL identifiers |
| if (!identifiers.isEmpty()) { |
| sb.append(identifiers); |
| } |
| |
| // Entity names |
| if (!entities.isEmpty()) { |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append(entities); |
| } |
| |
| // Identification variables |
| if (!identificationVariables.isEmpty()) { |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append(identificationVariables); |
| } |
| |
| // Range identification variables |
| if (!rangeIdentificationVariables.isEmpty()) { |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append(rangeIdentificationVariables); |
| } |
| |
| // Mappings |
| if (!mappings.isEmpty()) { |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append(mappings); |
| } |
| |
| // Class names |
| for (String className : classNames()) { |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append(className); |
| } |
| |
| // Table names |
| for (String tableName : tableNames()) { |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append(tableName); |
| } |
| |
| // Column names |
| for (String columnName : columnNames()) { |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append(columnName); |
| } |
| |
| // Enum constant names |
| for (EnumProposals enumProposals : enumConstant()) { |
| for (String enumConstant : enumProposals.enumConstants()) { |
| if (sb.length() > 0) { |
| sb.append(", "); |
| } |
| sb.append(enumConstant); |
| } |
| } |
| |
| // No proposals |
| if (sb.length() == 0) { |
| sb.append("<No Default Proposals>"); |
| } |
| |
| return sb.toString(); |
| } |
| |
| private static final class DefaultEnumProposals implements EnumProposals { |
| |
| private Set<String> constants; |
| private IType enumType; |
| |
| DefaultEnumProposals(IType enumType) { |
| super(); |
| this.enumType = enumType; |
| this.constants = new HashSet<String>(); |
| } |
| |
| @Override |
| public Iterable<String> enumConstants() { |
| return new SnapshotCloneIterable<String>(constants); |
| } |
| |
| @Override |
| public IType enumType() { |
| return enumType; |
| } |
| } |
| |
| /** |
| * This contains the result of inserting a proposal into a JPQL query at a given position. |
| * |
| * @see ContentAssistProposals#buildEscapedQuery(String, String, int, boolean) |
| * @see ContentAssistProposals#buildQuery(String, String, int, boolean) |
| */ |
| private static final class Result implements ResultQuery { |
| |
| /** |
| * The new JPQL query after insertion of the proposal. |
| */ |
| private String jpqlQuery; |
| |
| /** |
| * The position of the cursor within the new query. |
| */ |
| private int position; |
| |
| /** |
| * Creates a new <code>Result</code>. |
| * |
| * @param jpqlQuery The new JPQL query after insertion of the proposal |
| * @param position The position of the cursor within the new query |
| */ |
| public Result(String jpqlQuery, int position) { |
| super(); |
| this.jpqlQuery = jpqlQuery; |
| this.position = position; |
| } |
| |
| @Override |
| public int getPosition() { |
| return position; |
| } |
| |
| @Override |
| public String getQuery() { |
| return jpqlQuery; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("Query=[").append(jpqlQuery); |
| sb.append("], position=").append(position); |
| return sb.toString(); |
| } |
| } |
| } |