| /* |
| * Copyright (c) 2011, 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.model.query; |
| |
| import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.COMMA; |
| import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.SPACE; |
| |
| import java.io.IOException; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.persistence.jpa.jpql.Assert; |
| import org.eclipse.persistence.jpa.jpql.ExpressionTools; |
| import org.eclipse.persistence.jpa.jpql.parser.Expression; |
| import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar; |
| import org.eclipse.persistence.jpa.jpql.parser.VirtualJPQLQueryBNF; |
| import org.eclipse.persistence.jpa.jpql.tools.TypeHelper; |
| import org.eclipse.persistence.jpa.jpql.tools.model.DefaultProblem; |
| import org.eclipse.persistence.jpa.jpql.tools.model.IJPQLQueryBuilder; |
| import org.eclipse.persistence.jpa.jpql.tools.model.IPropertyChangeListener; |
| import org.eclipse.persistence.jpa.jpql.tools.model.Problem; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedTypeProvider; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.IType; |
| import org.eclipse.persistence.jpa.jpql.tools.spi.ITypeRepository; |
| import org.eclipse.persistence.jpa.jpql.tools.utility.iterable.SnapshotCloneIterable; |
| import org.eclipse.persistence.jpa.jpql.utility.CollectionTools; |
| |
| /** |
| * The abstract definition of a {@link StateObject}. |
| * |
| * @version 2.5 |
| * @since 2.4 |
| * @author Pascal Filion |
| */ |
| @SuppressWarnings("nls") |
| public abstract class AbstractStateObject implements StateObject { |
| |
| /** |
| * The object responsible to actually register the listeners and to notify them upon changes made |
| * to this {@link StateObject}. |
| */ |
| private ChangeSupport changeSupport; |
| |
| /** |
| * The {@link StateObject} that is decorating this one by changing its behavior or <code>null</code> |
| * if none was set. |
| */ |
| private StateObject decorator; |
| |
| /** |
| * The parsed object when a JPQL query is parsed and converted into a {@link StateObject} or |
| * <code>null</code> when the JPQL query is manually created (i.e. not from a string). |
| */ |
| private Expression expression; |
| |
| /** |
| * The parent of this state object. |
| */ |
| private StateObject parent; |
| |
| /** |
| * Creates a new <code>AbstractStateObject</code>. |
| * |
| * @param parent The parent of this state object, which cannot be <code>null</code> |
| * @exception NullPointerException The given parent cannot be <code>null</code>, unless {@link |
| * #changeSupport} is overridden and does not throw the exception |
| */ |
| protected AbstractStateObject(StateObject parent) { |
| super(); |
| this.parent = checkParent(parent); |
| initialize(); |
| } |
| |
| /** |
| * The given {@link StateObjectVisitor} needs to visit this class but it is defined by a |
| * third-party provider. This method will programmatically invoke the <b>visit</b> method defined |
| * on the given visitor which signature should be. |
| * |
| * <div><code>{public|protected|private} void visit(ThirdPartyStateObject stateObject)</code></div> |
| * <p> |
| * or |
| * |
| * <div><p><code>{public|protected|private} void visit(StateObject stateObject)</code></p></div> |
| * |
| * @param visitor The {@link StateObjectVisitor} to visit this {@link StateObject} programmatically |
| * @return <code>true</code> if the call was successfully executed; <code>false</code> otherwise |
| * @since 2.4 |
| */ |
| protected boolean acceptUnknownVisitor(StateObjectVisitor visitor) { |
| try { |
| try { |
| acceptUnknownVisitor(visitor, visitor.getClass(), getClass()); |
| } |
| catch (NoSuchMethodException e) { |
| // Try with Expression has the parameter type |
| acceptUnknownVisitor(visitor, visitor.getClass(), StateObject.class); |
| } |
| return true; |
| } |
| catch (NoSuchMethodException e) { |
| // Ignore, just do nothing |
| return false; |
| } |
| catch (IllegalAccessException e) { |
| // Ignore, just do nothing |
| return false; |
| } |
| catch (InvocationTargetException e) { |
| Throwable cause = e.getCause(); |
| RuntimeException actual; |
| if (cause instanceof RuntimeException) { |
| actual = (RuntimeException) cause; |
| } |
| else { |
| actual = new RuntimeException(cause); |
| } |
| throw actual; |
| } |
| } |
| |
| /** |
| * The given {@link StateObjectVisitor} needs to visit this class but it is defined by a |
| * third-party provider. This method will programmatically invoke the <b>visit</b> method defined |
| * on the given visitor which signature should be. |
| * |
| * <div><p><code>{public|protected|private} void visit(ThirdPartyStateObject stateObject)</code></p></div> |
| * or |
| * |
| * <div><p><code>{public|protected|private} void visit(StateObject stateObject)</code></p></div> |
| * |
| * @param visitor The {@link StateObjectVisitor} to visit this {@link StateObject} programmatically |
| * @param type The type found in the hierarchy of the given {@link StateObjectVisitor} that will |
| * be used to retrieve the visit method |
| * @param parameterType The parameter type of the visit method |
| * @see #acceptUnknownVisitor(StateObjectVisitor) |
| * @since 2.4 |
| */ |
| protected void acceptUnknownVisitor(StateObjectVisitor visitor, |
| Class<?> type, |
| Class<?> parameterType) throws NoSuchMethodException, |
| IllegalAccessException, |
| InvocationTargetException{ |
| |
| try { |
| Method visitMethod = type.getDeclaredMethod("visit", parameterType); |
| visitMethod.setAccessible(true); |
| visitMethod.invoke(visitor, this); |
| } |
| catch (NoSuchMethodException e) { |
| type = type.getSuperclass(); |
| if (type == Object.class) { |
| throw e; |
| } |
| else { |
| acceptUnknownVisitor(visitor, type, parameterType); |
| } |
| } |
| } |
| |
| /** |
| * Adds the children of this {@link StateObject} to the given list. |
| * |
| * @param children The list used to store the children |
| */ |
| protected void addChildren(List<StateObject> children) { |
| } |
| |
| /** |
| * Adds to the given list the problems that were found with the current state of this {@link |
| * StateObject}, which means there are validation issues. |
| * |
| * @param problems The list to which the problems are added |
| */ |
| protected void addProblems(List<Problem> problems) { |
| } |
| |
| @Override |
| public final void addPropertyChangeListener(String propertyName, IPropertyChangeListener<?> listener) { |
| changeSupport.addPropertyChangeListener(propertyName, listener); |
| } |
| |
| /** |
| * Determines whether the given two {@link StateObject} are equivalent, i.e. the information of |
| * both {@link StateObject} is the same. |
| * |
| * @param stateObject1 The first {@link StateObject} to compare its content with the other one |
| * @param stateObject2 The second {@link StateObject} to compare its content with the other one |
| * @return <code>true</code> if both objects are equivalent; <code>false</code> otherwise |
| */ |
| protected final boolean areEquivalent(StateObject stateObject1, StateObject stateObject2) { |
| |
| // Both are equal or both are null |
| if ((stateObject1 == stateObject2) || (stateObject1 == null) && (stateObject2 == null)) { |
| return true; |
| } |
| |
| // One is null but the other is not |
| if ((stateObject1 == null) || (stateObject2 == null)) { |
| return false; |
| } |
| |
| return stateObject1.isEquivalent(stateObject2); |
| } |
| |
| /** |
| * Creates a new {@link Problem} describing a single issue found with the information contained |
| * in this {@link StateObject}. |
| * |
| * @param messageKey The key used to retrieve the localized message describing the problem found |
| * with the current state of this {@link StateObject} |
| * @return The new {@link Problem} |
| */ |
| protected final Problem buildProblem(String messageKey) { |
| return buildProblem(messageKey, ExpressionTools.EMPTY_STRING_ARRAY); |
| } |
| |
| /** |
| * Creates a new {@link Problem} describing a single issue found with the information contained |
| * in this {@link StateObject}. |
| * |
| * @param messageKey The key used to retrieve the localized message describing the problem found |
| * with the current state of this {@link StateObject} |
| * @param arguments A list of arguments that can be used to complete the message or an empty list |
| * if no additional information is necessary |
| * @return The new {@link Problem} |
| */ |
| protected final Problem buildProblem(String messageKey , String... arguments) { |
| return new DefaultProblem(this, messageKey, arguments); |
| } |
| |
| /** |
| * Parses the given JPQL fragment using the given JPQL query BNF. |
| * |
| * @param jpqlFragment A portion of a JPQL query that will be parsed and converted into a {@link |
| * StateObject} |
| * @param queryBNFId The unique identifier of the BNF that determines how to parse the fragment |
| * @return A {@link StateObject} representation of the given JPQL fragment |
| */ |
| @SuppressWarnings("unchecked") |
| protected <T extends StateObject> T buildStateObject(CharSequence jpqlFragment, String queryBNFId) { |
| return (T) getQueryBuilder().buildStateObject(this, jpqlFragment, queryBNFId); |
| } |
| |
| /** |
| * Parses the given JPQL fragment using the given JPQL query BNF. |
| * |
| * @param jpqlFragment A portion of a JPQL query that will be parsed and converted into either a |
| * single {@link StateObject} or a list of {@link StateObject}, which happens when the fragment |
| * contains a collection of items separated by either a comma or a space |
| * @param queryBNFId The unique identifier of the BNF that will be used to parse the fragment |
| * @return A list of {@link StateObject StateObjects} representing the given JPQL fragment, which |
| * means the list may contain a single {@link StateObject} or a multiple {@link StateObject |
| * StateObjects} if the fragment contains more than one expression of the same type. Example: |
| * "JOIN e.employees e LEFT JOIN e.address a", this would be parsed in two state objects |
| */ |
| @SuppressWarnings("unchecked") |
| protected <T extends StateObject> List<T> buildStateObjects(CharSequence jpqlFragment, |
| String queryBNFId) { |
| |
| VirtualJPQLQueryBNF queryBNF = new VirtualJPQLQueryBNF(getGrammar()); |
| queryBNF.setHandleCollection(true); |
| queryBNF.setFallbackBNFId(queryBNFId); |
| queryBNF.registerQueryBNF(queryBNFId); |
| |
| final List<StateObject> items = new ArrayList<StateObject>(); |
| |
| try { |
| StateObject stateObject = buildStateObject(jpqlFragment, queryBNF.getId()); |
| |
| StateObjectVisitor visitor = new AnonymousStateObjectVisitor() { |
| @SuppressWarnings("unused") |
| public void visit(CollectionExpressionStateObject stateObject) { |
| CollectionTools.addAll(items, stateObject.children()); |
| } |
| @Override |
| protected void visit(StateObject stateObject) { |
| items.add(stateObject); |
| } |
| }; |
| |
| stateObject.accept(visitor); |
| } |
| finally { |
| queryBNF.dispose(); |
| } |
| |
| return (List<T>) items; |
| } |
| |
| /** |
| * Checks whether the given parent is <code>null</code> or not. If it's <code>null</code> then |
| * throw a {@link NullPointerException}. |
| * |
| * @param parent The parent of this state object |
| * @return The given object |
| */ |
| protected StateObject checkParent(StateObject parent) { |
| Assert.isNotNull(parent, "The parent cannot be null"); |
| return parent; |
| } |
| |
| @Override |
| public final Iterable<StateObject> children() { |
| List<StateObject> children = new ArrayList<StateObject>(); |
| addChildren(children); |
| return new SnapshotCloneIterable<StateObject>(children); |
| } |
| |
| @Override |
| public void decorate(StateObject decorator) { |
| this.decorator = parent(decorator); |
| } |
| |
| @Override |
| public final boolean equals(Object object) { |
| return super.equals(object); |
| } |
| |
| @Override |
| public IdentificationVariableStateObject findIdentificationVariable(String identificationVariable) { |
| return parent.findIdentificationVariable(identificationVariable); |
| } |
| |
| /** |
| * Notifies the {@link IPropertyChangeListener IPropertyChangeListeners} that have been registered |
| * with the given property name that the property has changed. |
| * |
| * @param propertyName The name of the property associated with the property change |
| * @param oldValue The old value of the property that changed |
| * @param newValue The new value of the property that changed |
| */ |
| protected final void firePropertyChanged(String propertyName, Object oldValue, Object newValue) { |
| changeSupport.firePropertyChanged(propertyName, oldValue, newValue); |
| } |
| |
| /** |
| * Returns the object responsible to actually register the listeners and to notify them upon |
| * changes made to this {@link StateObject}. |
| * |
| * @return The manager of listeners and notification |
| */ |
| protected final ChangeSupport getChangeSupport() { |
| return changeSupport; |
| } |
| |
| @Override |
| public DeclarationStateObject getDeclaration() { |
| return parent.getDeclaration(); |
| } |
| |
| @Override |
| public StateObject getDecorator() { |
| return decorator; |
| } |
| |
| @Override |
| public Expression getExpression() { |
| return expression; |
| } |
| |
| @Override |
| public JPQLGrammar getGrammar() { |
| return getRoot().getGrammar(); |
| } |
| |
| @Override |
| public IManagedTypeProvider getManagedTypeProvider() { |
| return getRoot().getManagedTypeProvider(); |
| } |
| |
| @Override |
| public StateObject getParent() { |
| return parent; |
| } |
| |
| @Override |
| public IJPQLQueryBuilder getQueryBuilder() { |
| return getRoot().getQueryBuilder(); |
| } |
| |
| @Override |
| public JPQLQueryStateObject getRoot() { |
| return parent.getRoot(); |
| } |
| |
| /** |
| * Retrieves the external type for the given Java type. |
| * |
| * @param type The Java type to wrap with an external form |
| * @return The external form of the given type |
| */ |
| public IType getType(Class<?> type) { |
| return getTypeRepository().getType(type); |
| } |
| |
| /** |
| * Retrieves the external class for the given fully qualified class name. |
| * |
| * @param typeName The fully qualified class name of the class to retrieve |
| * @return The external form of the class to retrieve |
| */ |
| public IType getType(String typeName) { |
| return getTypeRepository().getType(typeName); |
| } |
| |
| /** |
| * Returns a helper that gives access to the most common {@link IType types}. |
| * |
| * @return A helper containing a collection of methods related to {@link IType} |
| */ |
| public TypeHelper getTypeHelper() { |
| return getTypeRepository().getTypeHelper(); |
| } |
| |
| /** |
| * Returns the type repository for the application. |
| * |
| * @return The repository of {@link IType ITypes} |
| */ |
| public ITypeRepository getTypeRepository() { |
| return getManagedTypeProvider().getTypeRepository(); |
| } |
| |
| @Override |
| public final int hashCode() { |
| return super.hashCode(); |
| } |
| |
| /** |
| * Initializes this state object. |
| */ |
| protected void initialize() { |
| changeSupport = new ChangeSupport(this); |
| } |
| |
| @Override |
| public boolean isDecorated() { |
| return decorator != null; |
| } |
| |
| @Override |
| public boolean isEquivalent(StateObject stateObject) { |
| return (this == stateObject) || |
| ((stateObject != null) && (stateObject.getClass() == getClass())); |
| } |
| |
| /** |
| * Makes sure the given list of {@link StateObject} has this one as its parent. |
| * |
| * @param stateObjects The list of {@link StateObject} to have this one as its parent |
| * @return The given list of {@link StateObject} |
| */ |
| protected <T extends StateObject> List<T> parent(List<T> stateObjects) { |
| for (StateObject stateObject : stateObjects) { |
| parent(stateObject); |
| } |
| return stateObjects; |
| } |
| |
| /** |
| * Makes sure the given list of {@link StateObject} has this one as its parent. |
| * |
| * @param stateObjects The list of {@link StateObject} to have this one as its parent |
| * @return The given list of {@link StateObject} |
| */ |
| protected <T extends StateObject> T[] parent(T... stateObjects) { |
| for (StateObject stateObject : stateObjects) { |
| parent(stateObject); |
| } |
| return stateObjects; |
| } |
| |
| /** |
| * Makes sure the given {@link StateObject} has this one as its parent. |
| * |
| * @param stateObject The {@link StateObject} to have this one as its parent |
| * @return The given {@link StateObject} |
| */ |
| protected <T extends StateObject> T parent(T stateObject) { |
| if (stateObject != null) { |
| stateObject.setParent(this); |
| } |
| return stateObject; |
| } |
| |
| @Override |
| public final void removePropertyChangeListener(String propertyName, IPropertyChangeListener<?> listener) { |
| changeSupport.removePropertyChangeListener(propertyName, listener); |
| } |
| |
| /** |
| * Sets the actual parsed object if this {@link StateObject} representation of the JPQL query |
| * is created by converting the parsed representation of the JPQL query. |
| * |
| * @param expression The parsed object when a JPQL query is parsed |
| */ |
| public void setExpression(Expression expression) { |
| this.expression = expression; |
| } |
| |
| @Override |
| public final void setParent(StateObject parent) { |
| Assert.isNotNull(parent, "The parent cannot be null"); |
| this.parent = parent; |
| } |
| |
| @Override |
| public final String toString() { |
| StringBuilder sb = new StringBuilder(); |
| toString(sb); |
| return sb.toString(); |
| } |
| |
| @Override |
| public final void toString(Appendable writer) { |
| try { |
| toStringInternal(writer); |
| } |
| catch (IOException e) { |
| // Never happens because the Appendable should be an AbstractStringBuilder |
| } |
| } |
| |
| /** |
| * Prints out a string representation of this {@link StateObject}. |
| * <p> |
| * <b>Important:</b> If this {@link StateObject} is decorated by another one, then {@link |
| * #toString(Appendable)} from that decorator is invoked, otherwise {@link #toTextInternal(Appendable)} |
| * from this one is invoked. |
| * |
| * @param writer The writer used to print out the string representation |
| * @throws IOException This should never happens, it is only required because |
| * {@link Appendable#append(CharSequence)} throws an {@link IOException} |
| */ |
| protected final void toStringInternal(Appendable writer) throws IOException { |
| if (isDecorated()) { |
| getDecorator().toString(writer); |
| } |
| else { |
| toTextInternal(writer); |
| } |
| } |
| |
| protected void toStringItems(Appendable writer, |
| List<? extends StateObject> items, |
| boolean useComma) throws IOException { |
| |
| int count = items.size(); |
| int index = -1; |
| |
| for (StateObject stateObject : items) { |
| stateObject.toString(writer); |
| |
| if (++index + 1 < count) { |
| if (useComma) { |
| writer.append(COMMA); |
| } |
| writer.append(SPACE); |
| } |
| } |
| } |
| |
| @Override |
| public final void toText(Appendable writer) { |
| try { |
| toTextInternal(writer); |
| } |
| catch (IOException e) { |
| // Never happens because the Appendable should be an AbstractStringBuilder |
| } |
| } |
| |
| /** |
| * Prints out a string representation of this {@link StateObject}, which should not be used to |
| * define a <code>true</code> string representation of a JPQL query but should be used for |
| * debugging purposes. |
| * |
| * @param writer The writer used to print out the string representation |
| * @throws IOException This should never happens, it is only required because {@link Appendable} |
| * is used instead of any concrete class |
| */ |
| protected abstract void toTextInternal(Appendable writer) throws IOException; |
| } |