blob: f75ca75dd5ef3e4b296acc1fe76f05a83e8562b3 [file] [log] [blame]
/*
* 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 java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.StringTokenizer;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.parser.AbstractPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableBNF;
import org.eclipse.persistence.jpa.jpql.tools.model.IListChangeEvent.EventType;
import org.eclipse.persistence.jpa.jpql.tools.model.IListChangeListener;
import org.eclipse.persistence.jpa.jpql.tools.model.ListChangeEvent;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedType;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedTypeProvider;
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.spi.ITypeDeclaration;
import org.eclipse.persistence.jpa.jpql.utility.CollectionTools;
import org.eclipse.persistence.jpa.jpql.utility.iterable.ListIterable;
import org.eclipse.persistence.jpa.jpql.utility.iterable.SnapshotCloneListIterable;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;
/**
* An identification variable followed by the navigation operator (.) and a state field or
* association field is a path expression. The type of the path expression is the type computed as
* the result of navigation; that is, the type of the state field or association field to which the
* expression navigates.
*
* @see AbstractPathExpression
*
* @version 2.5
* @since 2.4
* @author Pascal Filion
*/
@SuppressWarnings("nls")
public abstract class AbstractPathExpressionStateObject extends AbstractStateObject
implements ListHolderStateObject<String> {
/**
* The {@link StateObject} that represents the identification variable portion of the path
* expression, which can be one of the following when the JPA version is 2.0 or later:
* <ul>
* <li>{@link IdentificationVariableStateObject}</li>
* <li>{@link KeyExpressionStateObject}</li>
* <li>{@link ValueExpressionStateObject}</li>
* </ul>
*/
private StateObject identificationVariable;
/**
*
*/
private IManagedType managedType;
/**
*
*/
private ArrayList<IMapping> mappings;
/**
* The list of segments, including the general identification variable.
*/
private List<String> paths;
/**
*
*/
private boolean resolved;
/**
* The {@link IType} of the object being mapped to this identification variable.
*/
private IType type;
/**
* The {@link ITypeDeclaration} of the object being mapped to this identification variable.
*/
private ITypeDeclaration typeDeclaration;
/**
* Notifies the identification variable property has changed.
*/
public static final String IDENTIFICATION_VARIABLE_PROPERTY = "identificationVariable";
/**
* Notifies the content of the paths list has changed.
*/
public static final String PATHS_LIST = "paths";
/**
* Creates a new <code>AbstractPathExpressionStateObject</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>
*/
protected AbstractPathExpressionStateObject(StateObject parent) {
super(parent);
}
/**
* Creates a new <code>AbstractPathExpressionStateObject</code>.
*
* @param parent The parent of this state object, which cannot be <code>null</code>
* @param path The path expression
* @exception NullPointerException The given parent cannot be <code>null</code>
*/
protected AbstractPathExpressionStateObject(StateObject parent, String path) {
super(parent);
setPath(path);
}
@Override
protected void addChildren(List<StateObject> children) {
super.addChildren(children);
StateObject stateObject = getIdentificationVariable();
if (stateObject != null) {
children.add(stateObject);
}
}
@Override
@SuppressWarnings("unchecked")
public String addItem(String item) {
getChangeSupport().addItem(this, paths, PATHS_LIST, item);
return item;
}
@Override
public void addItems(List<? extends String> items) {
getChangeSupport().addItems(this, paths, PATHS_LIST, items);
}
@Override
public void addListChangeListener(String listName, IListChangeListener<String> listener) {
getChangeSupport().addListChangeListener(listName, listener);
}
/**
* Appends the given sequence of characters to the path expression. If the sequence does not
* begin with a dot, then the first segment will be appended to the last segment and then new
* segments will be created.
*
* @param text The sequence of characters to append to the path expression
*/
public void append(String text) {
StringBuilder word = new StringBuilder();
int pathCount = paths.size();
boolean appendToLastSegment = true;
int startIndex = pathCount;
for (int index = 0, count = text.length(); index < count; index++) {
char character = text.charAt(index);
if (character == DOT) {
if (word.length() > 0) {
// Append the content of the buffer to the end of the last segment
if (appendToLastSegment) {
String currentPath = paths.get(pathCount - 1);
paths.set(pathCount - 1, currentPath + word);
startIndex = pathCount - 1;
}
// Add a new segment
else {
paths.add(word.toString());
pathCount++;
}
// Clear the buffer
word.delete(0, word.length());
}
appendToLastSegment = false;
continue;
}
// Add the character to the buffer
else {
word.append(character);
}
}
if (word.length() > 0) {
// Append the content of the buffer to the end of the last segment
if (appendToLastSegment) {
String currentPath = paths.get(pathCount - 1);
paths.set(pathCount - 1, currentPath + word);
startIndex = pathCount - 1;
}
// Add a new segment
else {
paths.add(word.toString());
}
}
ListChangeEvent<String> event = new ListChangeEvent<String>(this, paths, EventType.CHANGED, PATHS_LIST, paths, startIndex, itemsSize());
getChangeSupport().fireListChangeEvent(event);
}
@Override
public boolean canMoveDown(String item) {
return false;
}
@Override
public boolean canMoveUp(String item) {
return false;
}
/**
* Clears the values related to the managed type and type.
*/
protected void clearResolvedObjects() {
mappings.clear();
resolved = false;
type = null;
typeDeclaration = null;
}
@Override
public AbstractPathExpression getExpression() {
return (AbstractPathExpression) super.getExpression();
}
/**
* Returns the {@link StateObject} representing the identification variable that starts the path
* expression, which can be a sample identification variable, a map value, map key or map entry
* expression.
*
* @return The root of the path expression
*/
public StateObject getIdentificationVariable() {
if ((identificationVariable == null) && hasItems()) {
identificationVariable = buildStateObject(getItem(0), IdentificationVariableBNF.ID);
}
return identificationVariable;
}
@Override
public String getItem(int index) {
return paths.get(index);
}
/**
* Returns
*
*/
public IManagedType getManagedType() {
if (managedType == null) {
managedType = resolveManagedType();
}
return managedType;
}
/**
* Returns
*
*/
public IMapping getMapping() {
resolveMappings();
return mappings.get(itemsSize() - 1);
}
/**
* Retrieves the {@link IMapping} for the path at the given position.
*
* @param index The index of the path for which its {@link IMapping} should be retrieved, which
* should start at 1 to skip the identification variable
*/
public IMapping getMapping(int index) {
resolveMappings();
return mappings.get(index);
}
/**
* Returns the string representation of the path expression. If the identification variable is
* virtual, then it is not part of the result.
*
* @return The path expression, which is never <code>null</code>
*/
public String getPath() {
return toString();
}
/**
* Returns the {@link IType} of the field handled by this object.
*
* @return Either the {@link IType} that was resolved by this state object or the {@link IType}
* for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
*/
public IType getType() {
if (type == null) {
type = resolveType();
}
return type;
}
/**
* Returns the {@link ITypeDeclaration} of the field handled by this object.
*
* @return Either the {@link ITypeDeclaration} that was resolved by this object or the {@link
* ITypeDeclaration} for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
*/
public ITypeDeclaration getTypeDeclaration() {
if (typeDeclaration == null) {
typeDeclaration = resolveTypeDeclaration();
}
return typeDeclaration;
}
/**
* Determines whether the identification variable is present.
*
* @return <code>true</code> the identification variable is present; <code>false</code> otherwise
*/
public boolean hasIdentificationVariable() {
return getIdentificationVariable() != null;
}
@Override
public boolean hasItems() {
return !paths.isEmpty();
}
@Override
protected void initialize() {
super.initialize();
paths = new ArrayList<String>();
mappings = new ArrayList<IMapping>();
}
@Override
public boolean isEquivalent(StateObject stateObject) {
if (!super.isEquivalent(stateObject)) {
return false;
}
AbstractPathExpressionStateObject path = (AbstractPathExpressionStateObject) stateObject;
if (!areEquivalent(getIdentificationVariable(), path.getIdentificationVariable())) {
return false;
}
int index = itemsSize();
if (index != path.itemsSize()) {
return false;
}
// Skip index 0, it is already tested and two identification
// variables with different case are equivalent
while (--index > 0) {
String path1 = getItem(index);
String path2 = path.getItem(index);
if (ExpressionTools.valuesAreDifferent(path1, path2)) {
return false;
}
}
return true;
}
@Override
public ListIterable<String> items() {
return new SnapshotCloneListIterable<String>(paths);
}
@Override
public int itemsSize() {
return paths.size();
}
@Override
public String moveDown(String item) {
throw new RuntimeException(getClass().getName() + " does not support moveDown(String).");
}
@Override
public String moveUp(String item) {
throw new RuntimeException(getClass().getName() + " does not support moveUp(String).");
}
/**
* Removes the single path at the given index.
*
* @param index The position of the single path to remove. If the index is 0, then the
* identification variable is nullified
*/
public void removeItem(int index) {
if (index == 0) {
setIdentificationVariableInternally(null);
}
removeItem(getItem(index));
}
@Override
public void removeItem(String item) {
getChangeSupport().removeItem(this, paths, PATHS_LIST, item);
}
@Override
public void removeItems(Collection<String> items) {
getChangeSupport().removeItems(this, this.paths, PATHS_LIST, items);
}
@Override
public void removeListChangeListener(String listName, IListChangeListener<String> listener) {
getChangeSupport().removeListChangeListener(listName, listener);
}
/**
* Resolves
*
*/
protected abstract IManagedType resolveManagedType();
/**
* Resolves the {@link IMapping} objects that constitutes the path expression.
*/
protected void resolveMappings() {
if (!resolved) {
resolved = true;
IManagedTypeProvider provider = getManagedTypeProvider();
IManagedType managedType = null;
for (int index = 0, count = itemsSize(); index < count; index++) {
// Identification variable
if (index == 0) {
StateObject stateObject = getIdentificationVariable();
// The identification variable is not set, which means the traversal can happen
if (stateObject != null) {
managedType = getDeclaration().findManagedType(stateObject);
}
mappings.add(null);
}
// Resolve the path expression after the identification variable
else if (managedType != null) {
String path = getItem(index);
// Cache the mapping
IMapping mapping = managedType.getMappingNamed(path);
mappings.add(mapping);
// Continue by retrieving the managed type
if (mapping != null) {
managedType = provider.getManagedType(mapping.getType());
}
else {
managedType = null;
}
}
else {
mappings.add(null);
}
}
}
}
/**
* Resolves the {@link IType} of the property handled by this object.
*
* @return Either the {@link IType} that was resolved by this object or the {@link IType} for
* {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
*/
protected abstract IType resolveType();
/**
* Resolves the {@link ITypeDeclaration} of the property handled by this object.
*
* @return Either the {@link ITypeDeclaration} that was resolved by this object or the {@link
* ITypeDeclaration} for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
*/
protected ITypeDeclaration resolveTypeDeclaration() {
resolveMappings();
return getMapping().getTypeDeclaration();
}
/**
* Sets the {@link StateObject} representing the identification variable that starts the path
* expression, which can be a sample identification variable, a map value, map key or map entry
* expression.
*
* @param identificationVariable The root of the path expression
*/
public void setIdentificationVariable(StateObject identificationVariable) {
setIdentificationVariableInternally(identificationVariable);
getChangeSupport().replaceItem(this, paths, PATHS_LIST, 0, identificationVariable.toString());
}
/**
* Sets the {@link StateObject} representing the identification variable that starts the path
* expression, which can be a sample identification variable, a map value, map key or map entry
* expression. This method does not replace the first path in the list of paths.
*
* @param identificationVariable The root of the path expression
*/
protected void setIdentificationVariableInternally(StateObject identificationVariable) {
clearResolvedObjects();
StateObject oldIdentificationVariable = this.identificationVariable;
this.identificationVariable = parent(identificationVariable);
firePropertyChanged(IDENTIFICATION_VARIABLE_PROPERTY, oldIdentificationVariable, identificationVariable);
}
/**
* Changes the path expression with the list of segments, the identification variable will also
* be updated with the first segment.
*
* @param path The new path expression
*/
public void setPath(CharSequence path) {
List<String> paths = new ArrayList<String>();
for (StringTokenizer tokenizer = new StringTokenizer(path.toString(), ".", true); tokenizer.hasMoreTokens(); ) {
String token = tokenizer.nextToken();
if (!token.equals(".")) {
paths.add(token);
}
else if (!tokenizer.hasMoreTokens()) {
paths.add(ExpressionTools.EMPTY_STRING);
}
}
setPaths(paths.listIterator());
}
/**
* Replaces the existing path segment to become the given one.
*
* @param index The position of the path segment to replace
* @param path The replacement
*/
public void setPath(int index, String path) {
if (index == 0) {
setIdentificationVariableInternally(null);
}
else {
clearResolvedObjects();
}
getChangeSupport().replaceItem(this, paths, PATHS_LIST, index, path);
}
/**
* Changes the path expression with the list of segments, the identification variable will also
* be updated with the first segment.
*
* @param paths The new path expression
*/
public void setPaths(List<String> paths) {
setIdentificationVariableInternally(null);
getChangeSupport().replaceItems(this, this.paths, PATHS_LIST, paths);
}
/**
* Changes the path expression with the list of segments, the identification variable will also
* be updated with the first segment.
*
* @param paths The new path expression
*/
public void setPaths(ListIterator<String> paths) {
setPaths(CollectionTools.list(paths));
}
/**
* Changes the path expression with the list of segments, the identification variable will also
* be updated with the first segment.
*
* @param paths The new path expression
*/
public void setPaths(String... paths) {
setPaths(Arrays.asList(paths));
}
@Override
protected void toTextInternal(Appendable writer) throws IOException {
StateObject stateObject = getIdentificationVariable();
if (stateObject != null) {
String variable = stateObject.toString();
if (variable.length() > 0) {
writer.append(variable);
if (hasItems()) {
writer.append(DOT);
}
}
}
for (int index = 1, count = paths.size(); index < count; index++) {
writer.append(paths.get(index));
if (index < count - 1) {
writer.append(DOT);
}
}
}
}