org.eclipse.persistence.jpa.jpql.parser.Join Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of eclipselink Show documentation
Show all versions of eclipselink Show documentation
EclipseLink build based upon Git transaction f2b9fc5
/*
* Copyright (c) 2006, 2018 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.parser;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import org.eclipse.persistence.jpa.jpql.WordParser;
/**
* A JOIN enables the fetching of an association as a side effect of the execution of a query.
* A JOIN is specified over an entity and its related entities.
*
* BNF: join ::= join_spec join_association_path_expression [AS] identification_variable
*
* A JOIN FETCH enables the fetching of an association as a side effect of the execution of
* a query. A JOIN FETCH is specified over an entity and its related entities.
*
*
BNF: fetch_join ::= join_spec FETCH join_association_path_expression
*
* EclipseLink 2.4
*
*
BNF: join_spec { abstract_schema_name | join_association_path_expression } [AS] identification_variable [join_condition]
*
* @version 2.5
* @since 2.3
* @author Pascal Filion
*/
@SuppressWarnings("nls")
public final class Join extends AbstractExpression {
/**
* The actual AS identifier found in the string representation of the JPQL query.
*/
private String asIdentifier;
/**
* Determines whether a whitespace was parsed after AS.
*/
private boolean hasSpaceAfterAs;
/**
* Determines whether a whitespace was parsed after the identification variable.
*/
private boolean hasSpaceAfterIdentificationVariable;
/**
* Determines whether a whitespace was parsed after JOIN.
*/
private boolean hasSpaceAfterJoin;
/**
* Determines whether a whitespace was parsed after the join association path expression.
*/
private boolean hasSpaceAfterJoinAssociation;
/**
* The {@link Expression} representing the identification variable.
*/
private AbstractExpression identificationVariable;
/**
* The {@link Expression} representing the join association path expression.
*/
private AbstractExpression joinAssociationPath;
/**
* The actual JOIN identifier found in the string representation of the JPQL query.
*/
private String joinIdentifier;
/**
* The {@link Expression} representing the join ON clause.
*/
private AbstractExpression onClause;
/**
* Make sure when parsing the identification variable with tolerance turned on, a the join
* condition identifier ON is not parsed.
*/
private boolean parsingIdentificationVariable;
/**
* Creates a new Join
.
*
* @param parent The parent of this expression
* @param identifier The full JOIN identifier
*/
public Join(AbstractExpression parent, String identifier) {
super(parent, identifier);
}
/**
* {@inheritDoc}
*/
public void accept(ExpressionVisitor visitor) {
visitor.visit(this);
}
/**
* {@inheritDoc}
*/
public void acceptChildren(ExpressionVisitor visitor) {
getJoinAssociationPath().accept(visitor);
getIdentificationVariable().accept(visitor);
getOnClause().accept(visitor);
}
/**
* {@inheritDoc}
*/
@Override
protected void addChildrenTo(Collection children) {
children.add(getJoinAssociationPath());
children.add(getIdentificationVariable());
children.add(getOnClause());
}
/**
* {@inheritDoc}
*/
@Override
protected void addOrderedChildrenTo(List children) {
String join = getText();
String space = " ";
// Break the identifier into multiple identifiers
if (join.indexOf(space) != -1) {
StringTokenizer tokenizer = new StringTokenizer(join, space, true);
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
children.add(buildStringExpression(token));
}
}
else {
children.add(buildStringExpression(join));
}
if (hasSpaceAfterJoin) {
children.add(buildStringExpression(SPACE));
}
// Join association path
if (joinAssociationPath != null) {
children.add(joinAssociationPath);
}
if (hasSpaceAfterJoinAssociation) {
children.add(buildStringExpression(SPACE));
}
// 'AS'
if (asIdentifier != null) {
children.add(buildStringExpression(AS));
if (hasSpaceAfterAs) {
children.add(buildStringExpression(SPACE));
}
}
// Identification variable
if (identificationVariable != null) {
children.add(identificationVariable);
}
if (hasSpaceAfterIdentificationVariable) {
children.add(buildStringExpression(SPACE));
}
// ON clause
if (onClause != null) {
children.add(onClause);
}
}
/**
* {@inheritDoc}
*/
@Override
public JPQLQueryBNF findQueryBNF(Expression expression) {
if ((joinAssociationPath != null) && joinAssociationPath.isAncestor(expression)) {
return getQueryBNF(JoinAssociationPathExpressionBNF.ID);
}
if ((identificationVariable != null) && identificationVariable.isAncestor(expression)) {
return getQueryBNF(IdentificationVariableBNF.ID);
}
if ((onClause != null) && onClause.isAncestor(expression)) {
return getQueryBNF(OnClauseBNF.ID);
}
return super.findQueryBNF(expression);
}
/**
* Returns the actual AS found in the string representation of the JPQL query, which has
* the actual case that was used.
*
* @return The AS identifier that was actually parsed, or an empty string if it was not
* parsed
*/
public String getActualAsIdentifier() {
return asIdentifier;
}
/**
* Returns the actual identifier found in the string representation of the JPQL query, which has
* the actual case that was used.
*
* @return The identifier identifier that was actually parsed
*/
public String getActualIdentifier() {
return joinIdentifier;
}
/**
* Returns the {@link Expression} that represents the identification variable.
*
* @return The expression that was parsed representing the identification variable
*/
public Expression getIdentificationVariable() {
if (identificationVariable == null) {
identificationVariable = buildNullExpression();
}
return identificationVariable;
}
/**
* Returns the identifier this expression represents.
*
* @return Either JOIN, INNER JOIN, LEFT JOIN or LEFT OUTER JOIN.
* Although it's possible to have an incomplete identifier if the query is not complete
*/
public String getIdentifier() {
return getText();
}
/**
* Returns the {@link Expression} that represents the join association path expression.
*
* @return The expression that was parsed representing the join association path expression
*/
public Expression getJoinAssociationPath() {
if (joinAssociationPath == null) {
joinAssociationPath = buildNullExpression();
}
return joinAssociationPath;
}
/**
* Returns the {@link Expression} that represents the ON clause if present.
*
* @return The expression that was parsed representing the identification variable
*/
public Expression getOnClause() {
if (onClause == null) {
onClause = buildNullExpression();
}
return onClause;
}
/**
* {@inheritDoc}
*/
public JPQLQueryBNF getQueryBNF() {
return getQueryBNF(JoinBNF.ID);
}
/**
* Determines whether the identifier AS was parsed.
*
* @return true
if the identifier AS was parsed; false
otherwise
*/
public boolean hasAs() {
return asIdentifier != null;
}
/**
* Determines whether the identifier FETCH was parsed.
*
* @return true
if the identifier FETCH was parsed; false
otherwise
*/
public boolean hasFetch() {
String identifier = getText();
return identifier == JOIN_FETCH ||
identifier == INNER_JOIN_FETCH ||
identifier == LEFT_JOIN_FETCH ||
identifier == LEFT_OUTER_JOIN_FETCH;
}
/**
* Determines whether the identification variable was parsed.
*
* @return true
if the identification variable was parsed; false
otherwise
*/
public boolean hasIdentificationVariable() {
return identificationVariable != null &&
!identificationVariable.isNull() &&
!identificationVariable.isVirtual();
}
/**
* Determines whether the join association path expression was parsed.
*
* @return true
if the join association path expression was parsed; false
* otherwise
*/
public boolean hasJoinAssociationPath() {
return joinAssociationPath != null &&
!joinAssociationPath.isNull();
}
/**
* Determines whether the ON clause was parsed.
*
* @return true
if the ON clause was parsed; false
otherwise
*/
public boolean hasOnClause() {
return onClause != null &&
!onClause.isNull();
}
/**
* Determines whether a whitespace was parsed after AS.
*
* @return true
if there was a whitespace after AS; false
* otherwise
*/
public boolean hasSpaceAfterAs() {
return hasSpaceAfterAs;
}
/**
* Determines whether a whitespace was parsed before ON.
*
* @return true
if there was a whitespace before ON; false
* otherwise
*/
public boolean hasSpaceAfterIdentificationVariable() {
return hasSpaceAfterIdentificationVariable;
}
/**
* Determines whether a whitespace was parsed after JOIN.
*
* @return true
if there was a whitespace after JOIN; false
otherwise
*/
public boolean hasSpaceAfterJoin() {
return hasSpaceAfterJoin;
}
/**
* Determines whether a whitespace was parsed after the join association path expression.
*
* @return true
if there was a whitespace after join association path expression;
* false
otherwise
*/
public boolean hasSpaceAfterJoinAssociation() {
return hasSpaceAfterJoinAssociation;
}
/**
* Determines whether this {@link Join} is a left join, i.e. {@link Expression#LEFT_JOIN},
* {@link Expression#LEFT_JOIN_FETCH}, {@link Expression#LEFT_OUTER_JOIN}, {@link
* Expression#LEFT_OUTER_JOIN_FETCH}.
*
* @return true
if this {@link Join} expression is a {@link Expression#LEFT_JOIN},
* {@link Expression#LEFT_JOIN_FETCH}, {@link Expression#LEFT_OUTER_JOIN}, {@link
* Expression#LEFT_OUTER_JOIN_FETCH}; false
otherwise
*/
public boolean isLeftJoin() {
String identifier = getIdentifier();
return identifier == LEFT_JOIN ||
identifier == LEFT_OUTER_JOIN ||
identifier == LEFT_JOIN_FETCH ||
identifier == LEFT_OUTER_JOIN_FETCH;
}
/**
* {@inheritDoc}
*/
@Override
protected boolean isParsingComplete(WordParser wordParser, String word, Expression expression) {
// Make sure when parsing the identification variable with tolerance turned on,
// a the join condition identifier ON is not parsed
if (parsingIdentificationVariable &&
word.equalsIgnoreCase(ON)) {
return true;
}
return word.equalsIgnoreCase(AS) ||
word.equalsIgnoreCase(INNER) ||
word.equalsIgnoreCase(JOIN) ||
word.equalsIgnoreCase(LEFT) ||
word.equalsIgnoreCase(OUTER) ||
super.isParsingComplete(wordParser, word, expression);
}
/**
* {@inheritDoc}
*/
@Override
protected void parse(WordParser wordParser, boolean tolerant) {
// Parse the JOIN identifier
joinIdentifier = wordParser.moveForward(getText());
hasSpaceAfterJoin = wordParser.skipLeadingWhitespace() > 0;
// Parse the join association path expression
if (tolerant) {
joinAssociationPath = parse(wordParser, JoinAssociationPathExpressionBNF.ID, tolerant);
}
// TREAT expression
else if (wordParser.startsWithIdentifier(TREAT)) {
joinAssociationPath = new TreatExpression(this);
joinAssociationPath.parse(wordParser, tolerant);
}
// Abstract schema name, Collection-valued path expression or state field path expression
else {
String path = wordParser.word();
if (path.indexOf(".") == -1) {
joinAssociationPath = new AbstractSchemaName(this, path);
joinAssociationPath.parse(wordParser, tolerant);
}
else {
joinAssociationPath = new CollectionValuedPathExpression(this, path);
joinAssociationPath.parse(wordParser, tolerant);
}
}
int count = wordParser.skipLeadingWhitespace();
hasSpaceAfterJoinAssociation = count > 0;
// Parse 'AS'
if (wordParser.startsWithIdentifier(AS)) {
asIdentifier = wordParser.moveForward(AS);
hasSpaceAfterAs = wordParser.skipLeadingWhitespace() > 0;
}
// Parse the identification variable
parsingIdentificationVariable = true;
if (tolerant) {
identificationVariable = parse(wordParser, IdentificationVariableBNF.ID, tolerant);
}
else {
String word = wordParser.word();
if ((word.length() > 0) && !isParsingComplete(wordParser, word, null)) {
identificationVariable = new IdentificationVariable(this, word);
identificationVariable.parse(wordParser, tolerant);
}
}
parsingIdentificationVariable = false;
// A JOIN FETCH without '[AS] identification_variable' will not keep the
// whitespace after the join association for backward compatibility (for now)
if (asIdentifier == null &&
hasSpaceAfterJoinAssociation &&
identificationVariable == null &&
hasFetch() &&
!wordParser.startsWithIdentifier(ON)) {
hasSpaceAfterJoinAssociation = false;
wordParser.moveBackward(count);
count = 0;
}
else {
count = wordParser.skipLeadingWhitespace();
}
// Parse the ON clause
if (tolerant) {
onClause = parse(wordParser, OnClauseBNF.ID, tolerant);
}
else if (wordParser.startsWithIdentifier(ON)) {
onClause = new OnClause(this);
onClause.parse(wordParser, tolerant);
}
if (onClause != null) {
hasSpaceAfterIdentificationVariable = (count > 0);
}
else {
wordParser.moveBackward(count);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void toParsedText(StringBuilder writer, boolean actual) {
// Join identifier
writer.append(actual ? joinIdentifier : getText());
if (hasSpaceAfterJoin) {
writer.append(SPACE);
}
// Join association path
if (joinAssociationPath != null) {
joinAssociationPath.toParsedText(writer, actual);
}
if (hasSpaceAfterJoinAssociation) {
writer.append(SPACE);
}
// 'AS'
if (asIdentifier != null) {
writer.append(actual ? asIdentifier : AS);
if (hasSpaceAfterAs) {
writer.append(SPACE);
}
}
// Identification variable
if (identificationVariable != null) {
identificationVariable.toParsedText(writer, actual);
}
if (hasSpaceAfterIdentificationVariable) {
writer.append(SPACE);
}
// ON clause
if (onClause != null) {
onClause.toParsedText(writer, actual);
}
}
}