/******************************************************************************* * Copyright (c) 2011, 2015 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 v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * 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><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><code>{public|protected|private} void visit(ThirdPartyStateObject stateObject)</code></div> * <p> * or * * <div><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) { } /** * {@inheritDoc} */ @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; } /** * {@inheritDoc} */ @Override public final Iterable<StateObject> children() { List<StateObject> children = new ArrayList<StateObject>(); addChildren(children); return new SnapshotCloneIterable<StateObject>(children); } /** * {@inheritDoc} */ @Override public void decorate(StateObject decorator) { this.decorator = parent(decorator); } /** * {@inheritDoc} */ @Override public final boolean equals(Object object) { return super.equals(object); } /** * {@inheritDoc} */ @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; } /** * {@inheritDoc} */ @Override public DeclarationStateObject getDeclaration() { return parent.getDeclaration(); } /** * {@inheritDoc} */ @Override public StateObject getDecorator() { return decorator; } /** * {@inheritDoc} */ @Override public Expression getExpression() { return expression; } /** * {@inheritDoc} */ @Override public JPQLGrammar getGrammar() { return getRoot().getGrammar(); } /** * {@inheritDoc} */ @Override public IManagedTypeProvider getManagedTypeProvider() { return getRoot().getManagedTypeProvider(); } /** * {@inheritDoc} */ @Override public StateObject getParent() { return parent; } /** * {@inheritDoc} */ @Override public IJPQLQueryBuilder getQueryBuilder() { return getRoot().getQueryBuilder(); } /** * {@inheritDoc} */ @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(); } /** * {@inheritDoc} */ @Override public final int hashCode() { return super.hashCode(); } /** * Initializes this state object. */ protected void initialize() { changeSupport = new ChangeSupport(this); } /** * {@inheritDoc} */ @Override public boolean isDecorated() { return decorator != null; } /** * {@inheritDoc} */ @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; } /** * {@inheritDoc} */ @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; } /** * {@inheritDoc} */ @Override public final void setParent(StateObject parent) { Assert.isNotNull(parent, "The parent cannot be null"); this.parent = parent; } /** * {@inheritDoc} */ @Override public final String toString() { StringBuilder sb = new StringBuilder(); toString(sb); return sb.toString(); } /** * {@inheritDoc} */ @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); } } } /** * {@inheritDoc} */ @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; }