org.modeshape.jcr.query.QueryBuilder Maven / Gradle / Ivy
/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.jcr.query;
import java.math.BigDecimal;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.jcr.Binary;
import org.modeshape.common.annotation.NotThreadSafe;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.GraphI18n;
import org.modeshape.jcr.api.query.qom.Operator;
import org.modeshape.jcr.api.value.DateTime;
import org.modeshape.jcr.query.model.AllNodes;
import org.modeshape.jcr.query.model.And;
import org.modeshape.jcr.query.model.ArithmeticOperand;
import org.modeshape.jcr.query.model.ArithmeticOperator;
import org.modeshape.jcr.query.model.Between;
import org.modeshape.jcr.query.model.BindVariableName;
import org.modeshape.jcr.query.model.ChildCount;
import org.modeshape.jcr.query.model.ChildNode;
import org.modeshape.jcr.query.model.ChildNodeJoinCondition;
import org.modeshape.jcr.query.model.Column;
import org.modeshape.jcr.query.model.Comparison;
import org.modeshape.jcr.query.model.Constraint;
import org.modeshape.jcr.query.model.DescendantNode;
import org.modeshape.jcr.query.model.DescendantNodeJoinCondition;
import org.modeshape.jcr.query.model.DynamicOperand;
import org.modeshape.jcr.query.model.EquiJoinCondition;
import org.modeshape.jcr.query.model.FullTextSearch;
import org.modeshape.jcr.query.model.FullTextSearchScore;
import org.modeshape.jcr.query.model.Join;
import org.modeshape.jcr.query.model.JoinCondition;
import org.modeshape.jcr.query.model.JoinType;
import org.modeshape.jcr.query.model.Length;
import org.modeshape.jcr.query.model.Limit;
import org.modeshape.jcr.query.model.Literal;
import org.modeshape.jcr.query.model.LowerCase;
import org.modeshape.jcr.query.model.NamedSelector;
import org.modeshape.jcr.query.model.NodeDepth;
import org.modeshape.jcr.query.model.NodeLocalName;
import org.modeshape.jcr.query.model.NodeName;
import org.modeshape.jcr.query.model.NodePath;
import org.modeshape.jcr.query.model.Not;
import org.modeshape.jcr.query.model.NullOrder;
import org.modeshape.jcr.query.model.Or;
import org.modeshape.jcr.query.model.Order;
import org.modeshape.jcr.query.model.Ordering;
import org.modeshape.jcr.query.model.PropertyExistence;
import org.modeshape.jcr.query.model.PropertyValue;
import org.modeshape.jcr.query.model.Query;
import org.modeshape.jcr.query.model.QueryCommand;
import org.modeshape.jcr.query.model.ReferenceValue;
import org.modeshape.jcr.query.model.SameNode;
import org.modeshape.jcr.query.model.SameNodeJoinCondition;
import org.modeshape.jcr.query.model.Selector;
import org.modeshape.jcr.query.model.SelectorName;
import org.modeshape.jcr.query.model.SetCriteria;
import org.modeshape.jcr.query.model.SetQuery;
import org.modeshape.jcr.query.model.SetQuery.Operation;
import org.modeshape.jcr.query.model.Source;
import org.modeshape.jcr.query.model.StaticOperand;
import org.modeshape.jcr.query.model.Subquery;
import org.modeshape.jcr.query.model.TypeSystem;
import org.modeshape.jcr.query.model.UpperCase;
import org.modeshape.jcr.query.model.Visitors;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PropertyType;
/**
* A component that can be used to programmatically create {@link QueryCommand} objects. Simply call methods to build the selector
* clause, from clause, join criteria, where criteria, limits, and ordering, and then {@link #query() obtain the query}. This
* builder should be adequate for most queries; however, any query that cannot be expressed by this builder can always be
* constructed by directly creating the Abstract Query Model classes.
*
* This builder is stateful and therefore should only be used by one thread at a time. However, once a query has been built, the
* builder can be {@link #clear() cleared} and used to create another query.
*
*
* The order in which the methods are called are (for the most part) important. Simply call the methods in the same order that
* would be most natural in a normal SQL query. For example, the following code creates a Query object that is equivalent to "
* SELECT * FROM table
":
*
*
* QueryCommand query = builder.selectStar().from("table").query();
*
*
*
*
* Here are a few other examples:
*
*
* SQL Statement
* QueryBuilder code
*
*
*
*
*
* SELECT * FROM table1
* INNER JOIN table2
* ON table2.c0 = table1.c0
*
*
*
*
*
*
* query = builder.selectStar().from("table1").join("table2").on("table2.c0=table1.c0").query();
*
*
*
*
*
*
*
*
* SELECT * FROM table1 AS t1
* INNER JOIN table2 AS t2
* ON t1.c0 = t2.c0
*
*
*
*
*
*
* query = builder.selectStar().from("table1 AS t1").join("table2 AS t2").on("t1.c0=t2.c0").query();
*
*
*
*
*
*
*
*
* SELECT * FROM table1 AS t1
* INNER JOIN table2 AS t2
* ON t1.c0 = t2.c0
* INNER JOIN table3 AS t3
* ON t1.c1 = t3.c1
*
*
*
*
*
*
* query = builder.selectStar().from("table1 AS t1").innerJoin("table2 AS t2").on("t1.c0=t2.c0").innerJoin("table3 AS t3")
* .on("t1.c1=t3.c1").query();
*
*
*
*
*
*
*
*
* SELECT * FROM table1
* UNION
* SELECT * FROM table2
*
*
*
*
*
*
* query = builder.selectStar().from("table1").union().selectStar().from("table2").query();
*
*
*
*
*
*
*
*
* SELECT t1.c1,t1.c2,t2.c3 FROM table1 AS t1
* INNER JOIN table2 AS t2
* ON t1.c0 = t2.c0
* UNION ALL
* SELECT t3.c1,t3.c2,t4.c3 FROM table3 AS t3
* INNER JOIN table4 AS t4
* ON t3.c0 = t4.c0
*
*
*
*
*
*
* query = builder.select("t1.c1","t1.c2","t2.c3",)
* .from("table1 AS t1")
* .innerJoin("table2 AS t2")
* .on("t1.c0=t2.c0")
* .union()
* .select("t3.c1","t3.c2","t4.c3",)
* .from("table3 AS t3")
* .innerJoin("table4 AS t4")
* .on("t3.c0=t4.c0")
* .query();
*
*
*
*
*
*
*/
@NotThreadSafe
public class QueryBuilder {
protected final TypeSystem typeSystem;
protected Source source = new AllNodes();
protected Constraint constraint;
protected Listname [AS alias]
. Leading and trailing whitespace
* are trimmed.
*
* @param nameWithOptionalAlias the name and optional alias; may not be null
* @return the named selector object; never null
*/
protected NamedSelector namedSelector( String nameWithOptionalAlias ) {
String[] parts = nameWithOptionalAlias.split("\\s(AS|as)\\s");
if (parts.length == 2) {
return new NamedSelector(selector(parts[0]), selector(parts[1]));
}
return new NamedSelector(selector(parts[0]));
}
/**
* Create a {@link Column} given the supplied expression. The expression has the form "[tableName.]columnName
",
* where "tableName
" must be a valid table name or alias. If the table name/alias is not specified, then there is
* expected to be a single FROM clause with a single named selector.
*
* @param nameExpression the expression specifying the columm name and (optionally) the table's name or alias; may not be null
* @return the column; never null
* @throws IllegalArgumentException if the table's name/alias is not specified, but the query has more than one named source
*/
protected Column column( String nameExpression ) {
String[] parts = nameExpression.split("(?[tableName.]columnName", where " tableName
" must be a valid table name or alias. If the table
* name/alias is not specified, then there is expected to be a single FROM clause with a single named selector.
*
* @param columnNames the column expressions; may not be null
* @return this builder object, for convenience in method chaining
* @throws IllegalArgumentException if the table's name/alias is not specified, but the query has more than one named source
*/
public QueryBuilder select( String... columnNames ) {
for (String expression : columnNames) {
columns.add(column(expression));
}
return this;
}
/**
* Select all of the distinct values from the single-valued columns.
*
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder selectDistinctStar() {
distinct = true;
return selectStar();
}
/**
* Select the distinct values from the columns with the supplied names. Each column name has the form "
* [tableName.]columnName
", where " tableName
" must be a valid table name or alias. If the table
* name/alias is not specified, then there is expected to be a single FROM clause with a single named selector.
*
* @param columnNames the column expressions; may not be null
* @return this builder object, for convenience in method chaining
* @throws IllegalArgumentException if the table's name/alias is not specified, but the query has more than one named source
*/
public QueryBuilder selectDistinct( String... columnNames ) {
distinct = true;
return select(columnNames);
}
/**
* Specify that the query should select from the "__ALLNODES__" built-in table.
*
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder fromAllNodes() {
this.source = new AllNodes();
return this;
}
/**
* Specify that the query should select from the "__ALLNODES__" built-in table using the supplied alias.
*
* @param alias the alias for the "__ALL_NODES" table; may not be null
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder fromAllNodesAs( String alias ) {
AllNodes allNodes = new AllNodes(selector(alias));
SelectorName oldName = this.source instanceof Selector ? ((Selector)source).name() : null;
// Go through the columns and change the selector name to use the new alias ...
for (int i = 0; i != columns.size(); ++i) {
Column old = columns.get(i);
if (old.selectorName().equals(oldName)) {
columns.set(i, new Column(allNodes.aliasOrName(), old.getPropertyName(), old.getColumnName()));
}
}
this.source = allNodes;
return this;
}
/**
* Specify the name of the table from which tuples should be selected. The supplied string is of the form "
* tableName [AS alias]
".
*
* @param tableNameWithOptionalAlias the name of the table, optionally including the alias
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder from( String tableNameWithOptionalAlias ) {
Selector selector = namedSelector(tableNameWithOptionalAlias);
SelectorName oldName = this.source instanceof Selector ? ((Selector)source).name() : null;
// Go through the columns and change the selector name to use the new alias ...
for (int i = 0; i != columns.size(); ++i) {
Column old = columns.get(i);
if (old.selectorName().equals(oldName)) {
columns.set(i, new Column(selector.aliasOrName(), old.getPropertyName(), old.getColumnName()));
}
}
this.source = selector;
return this;
}
/**
* Begin the WHERE clause for this query by obtaining the constraint builder. When completed, be sure to call
* {@link ConstraintBuilder#end() end()} on the resulting constraint builder, or else the constraint will not be applied to
* the current query.
*
* @return the constraint builder that can be used to specify the criteria; never null
*/
public ConstraintBuilder where() {
return new ConstraintBuilder(null);
}
/**
* Perform an inner join between the already defined source with the supplied table. The supplied string is of the form "
* tableName [AS alias]
".
*
* @param tableName the name of the table, optionally including the alias
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause join( String tableName ) {
return innerJoin(tableName);
}
/**
* Perform an inner join between the already defined source with the supplied table. The supplied string is of the form "
* tableName [AS alias]
".
*
* @param tableName the name of the table, optionally including the alias
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause innerJoin( String tableName ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(tableName), JoinType.INNER);
}
/**
* Perform a cross join between the already defined source with the supplied table. The supplied string is of the form "
* tableName [AS alias]
". Cross joins have a higher precedent than other join types, so if this is called after
* another join was defined, the resulting cross join will be between the previous join's right-hand side and the supplied
* table.
*
* @param tableName the name of the table, optionally including the alias
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause crossJoin( String tableName ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(tableName), JoinType.CROSS);
}
/**
* Perform a full outer join between the already defined source with the supplied table. The supplied string is of the form "
* tableName [AS alias]
".
*
* @param tableName the name of the table, optionally including the alias
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause fullOuterJoin( String tableName ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(tableName), JoinType.FULL_OUTER);
}
/**
* Perform a left outer join between the already defined source with the supplied table. The supplied string is of the form "
* tableName [AS alias]
".
*
* @param tableName the name of the table, optionally including the alias
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause leftOuterJoin( String tableName ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(tableName), JoinType.LEFT_OUTER);
}
/**
* Perform a right outer join between the already defined source with the supplied table. The supplied string is of the form "
* tableName [AS alias]
".
*
* @param tableName the name of the table, optionally including the alias
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause rightOuterJoin( String tableName ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(tableName), JoinType.RIGHT_OUTER);
}
/**
* Perform an inner join between the already defined source with the "__ALLNODES__" table using the supplied alias.
*
* @param alias the alias for the "__ALL_NODES" table; may not be null
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause joinAllNodesAs( String alias ) {
return innerJoinAllNodesAs(alias);
}
/**
* Perform an inner join between the already defined source with the "__ALL_NODES" table using the supplied alias.
*
* @param alias the alias for the "__ALL_NODES" table; may not be null
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause innerJoinAllNodesAs( String alias ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(AllNodes.ALL_NODES_NAME + " AS " + alias), JoinType.INNER);
}
/**
* Perform a cross join between the already defined source with the "__ALL_NODES" table using the supplied alias. Cross joins
* have a higher precedent than other join types, so if this is called after another join was defined, the resulting cross
* join will be between the previous join's right-hand side and the supplied table.
*
* @param alias the alias for the "__ALL_NODES" table; may not be null
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause crossJoinAllNodesAs( String alias ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(AllNodes.ALL_NODES_NAME + " AS " + alias), JoinType.CROSS);
}
/**
* Perform a full outer join between the already defined source with the "__ALL_NODES" table using the supplied alias.
*
* @param alias the alias for the "__ALL_NODES" table; may not be null
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause fullOuterJoinAllNodesAs( String alias ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(AllNodes.ALL_NODES_NAME + " AS " + alias), JoinType.FULL_OUTER);
}
/**
* Perform a left outer join between the already defined source with the "__ALL_NODES" table using the supplied alias.
*
* @param alias the alias for the "__ALL_NODES" table; may not be null
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause leftOuterJoinAllNodesAs( String alias ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(AllNodes.ALL_NODES_NAME + " AS " + alias), JoinType.LEFT_OUTER);
}
/**
* Perform a right outer join between the already defined source with the "__ALL_NODES" table using the supplied alias.
*
* @param alias the alias for the "__ALL_NODES" table; may not be null
* @return the component that must be used to complete the join specification; never null
*/
public JoinClause rightOuterJoinAllNodesAs( String alias ) {
// Expect there to be a source already ...
return new JoinClause(namedSelector(AllNodes.ALL_NODES_NAME + " AS " + alias), JoinType.RIGHT_OUTER);
}
/**
* Specify the maximum number of rows that are to be returned in the results. By default there is no limit.
*
* @param rowLimit the maximum number of rows
* @return this builder object, for convenience in method chaining
* @throws IllegalArgumentException if the row limit is not a positive integer
*/
public QueryBuilder limit( int rowLimit ) {
this.limit.withRowLimit(rowLimit);
return this;
}
/**
* Specify the number of rows that results are to skip. The default offset is '0'.
*
* @param offset the number of rows before the results are to begin
* @return this builder object, for convenience in method chaining
* @throws IllegalArgumentException if the row limit is a negative integer
*/
public QueryBuilder offset( int offset ) {
this.limit.withOffset(offset);
return this;
}
/**
* Perform a UNION between the query as defined prior to this method and the query that will be defined following this method.
*
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder union() {
this.firstQuery = query();
this.firstQuerySetOperation = Operation.UNION;
this.firstQueryAll = false;
clear(false);
return this;
}
/**
* Perform a UNION ALL between the query as defined prior to this method and the query that will be defined following this
* method.
*
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder unionAll() {
this.firstQuery = query();
this.firstQuerySetOperation = Operation.UNION;
this.firstQueryAll = true;
clear(false);
return this;
}
/**
* Perform an INTERSECT between the query as defined prior to this method and the query that will be defined following this
* method.
*
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder intersect() {
this.firstQuery = query();
this.firstQuerySetOperation = Operation.INTERSECT;
this.firstQueryAll = false;
clear(false);
return this;
}
/**
* Perform an INTERSECT ALL between the query as defined prior to this method and the query that will be defined following
* this method.
*
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder intersectAll() {
this.firstQuery = query();
this.firstQuerySetOperation = Operation.INTERSECT;
this.firstQueryAll = true;
clear(false);
return this;
}
/**
* Perform an EXCEPT between the query as defined prior to this method and the query that will be defined following this
* method.
*
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder except() {
this.firstQuery = query();
this.firstQuerySetOperation = Operation.EXCEPT;
this.firstQueryAll = false;
clear(false);
return this;
}
/**
* Perform an EXCEPT ALL between the query as defined prior to this method and the query that will be defined following this
* method.
*
* @return this builder object, for convenience in method chaining
*/
public QueryBuilder exceptAll() {
this.firstQuery = query();
this.firstQuerySetOperation = Operation.EXCEPT;
this.firstQueryAll = true;
clear(false);
return this;
}
/**
* Obtain a builder that will create the order-by clause (with one or more {@link Ordering} statements) for the query. This
* method need be called only once to build the order-by clause, but can be called multiple times (it merely adds additional
* {@link Ordering} statements).
*
* @return the order-by builder; never null
*/
public OrderByBuilder orderBy() {
return new OrderByBuilder();
}
/**
* Return a {@link QueryCommand} representing the currently-built query.
*
* @return the resulting query command; never null
* @see #clear()
*/
public QueryCommand query() {
QueryCommand result = new Query(source, constraint, orderings, columns, limit, distinct);
if (this.firstQuery != null) {
// EXCEPT has a higher precedence than INTERSECT or UNION, so if the first query is
// an INTERSECT or UNION SetQuery, the result should be applied to the RHS of the previous set ...
if (firstQuery instanceof SetQuery && firstQuerySetOperation == Operation.EXCEPT) {
SetQuery setQuery = (SetQuery)firstQuery;
QueryCommand left = setQuery.getLeft();
QueryCommand right = setQuery.getRight();
SetQuery exceptQuery = new SetQuery(right, Operation.EXCEPT, result, firstQueryAll);
result = new SetQuery(left, setQuery.operation(), exceptQuery, setQuery.isAll());
} else {
result = new SetQuery(this.firstQuery, this.firstQuerySetOperation, result, this.firstQueryAll);
}
}
return result;
}
public interface OrderByOperandBuilder {
/**
* Adds to the order-by clause by using the length of the value for the given table and property.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param property the name of the property; may not be null and must refer to a valid property name
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder length( String table,
String property );
/**
* Adds to the order-by clause by using the value for the given table and property.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param property the name of the property; may not be null and must refer to a valid property name
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder propertyValue( String table,
String property );
/**
* Constrains the nodes in the the supplied table such that they must have a matching value for any of the node's
* reference properties.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder referenceValue( String table );
/**
* Constrains the nodes in the the supplied table such that they must have a matching value for the named property.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param property the name of the reference property; may be null if the constraint applies to all/any reference
* properties on the node
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder referenceValue( String table,
String property );
/**
* Adds to the order-by clause by using the full-text search score for the given table.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder fullTextSearchScore( String table );
/**
* Adds to the order-by clause by using the child count of the node given by the named table.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder childCount( String table );
/**
* Adds to the order-by clause by using the depth of the node given by the named table.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder depth( String table );
/**
* Adds to the order-by clause by using the path of the node given by the named table.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder path( String table );
/**
* Adds to the order-by clause by using the local name of the node given by the named table.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder nodeLocalName( String table );
/**
* Adds to the order-by clause by using the node name (including namespace) of the node given by the named table.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the order-by specification; never null
*/
public OrderByBuilder nodeName( String table );
/**
* Adds to the order-by clause by using the uppercase form of the next operand.
*
* @return the interface for completing the order-by specification; never null
*/
public OrderByOperandBuilder upperCaseOf();
/**
* Adds to the order-by clause by using the lowercase form of the next operand.
*
* @return the interface for completing the order-by specification; never null
*/
public OrderByOperandBuilder lowerCaseOf();
}
/**
* The component used to build the order-by clause. When the clause is completed, {@link #end()} should be called to return to
* the {@link QueryBuilder} instance.
*/
public class OrderByBuilder {
protected OrderByBuilder() {
}
/**
* Begin specifying an order-by specification using {@link Order#ASCENDING ascending order}.
*
* @return the interface for specifying the operand that is to be ordered; never null
*/
public OrderByOperandBuilder ascending() {
return new SingleOrderByOperandBuilder(this, Order.ASCENDING, NullOrder.NULLS_LAST);
}
/**
* Begin specifying an order-by specification using {@link Order#DESCENDING descending order}.
*
* @return the interface for specifying the operand that is to be ordered; never null
*/
public OrderByOperandBuilder descending() {
return new SingleOrderByOperandBuilder(this, Order.DESCENDING, NullOrder.NULLS_FIRST);
}
/**
* Begin specifying an order-by specification using {@link Order#ASCENDING ascending order}.
*
* @return the interface for specifying the operand that is to be ordered; never null
*/
public OrderByOperandBuilder ascendingNullsFirst() {
return new SingleOrderByOperandBuilder(this, Order.ASCENDING, NullOrder.NULLS_FIRST);
}
/**
* Begin specifying an order-by specification using {@link Order#DESCENDING descending order}.
*
* @return the interface for specifying the operand that is to be ordered; never null
*/
public OrderByOperandBuilder descendingNullsLast() {
return new SingleOrderByOperandBuilder(this, Order.DESCENDING, NullOrder.NULLS_LAST);
}
/**
* An optional convenience method that returns this builder, but which makes the code using this builder more readable.
*
* @return this builder; never null
*/
public OrderByBuilder then() {
return this;
}
/**
* Complete the order-by clause and return the QueryBuilder instance.
*
* @return the query builder instance; never null
*/
public QueryBuilder end() {
return QueryBuilder.this;
}
}
protected class SingleOrderByOperandBuilder implements OrderByOperandBuilder {
private final Order order;
private final NullOrder nullOrder;
private final OrderByBuilder builder;
protected SingleOrderByOperandBuilder( OrderByBuilder builder,
Order order,
NullOrder nullOrder ) {
this.order = order;
this.nullOrder = nullOrder;
this.builder = builder;
}
protected OrderByBuilder addOrdering( DynamicOperand operand ) {
Ordering ordering = new Ordering(operand, order, nullOrder);
QueryBuilder.this.orderings.add(ordering);
return builder;
}
@Override
public OrderByBuilder propertyValue( String table,
String property ) {
return addOrdering(new PropertyValue(selector(table), property));
}
@Override
public OrderByBuilder referenceValue( String table ) {
return addOrdering(new ReferenceValue(selector(table)));
}
@Override
public OrderByBuilder referenceValue( String table,
String property ) {
return addOrdering(new ReferenceValue(selector(table), property));
}
@Override
public OrderByBuilder length( String table,
String property ) {
return addOrdering(new Length(new PropertyValue(selector(table), property)));
}
@Override
public OrderByBuilder fullTextSearchScore( String table ) {
return addOrdering(new FullTextSearchScore(selector(table)));
}
@Override
public OrderByBuilder childCount( String table ) {
return addOrdering(new ChildCount(selector(table)));
}
@Override
public OrderByBuilder depth( String table ) {
return addOrdering(new NodeDepth(selector(table)));
}
@Override
public OrderByBuilder path( String table ) {
return addOrdering(new NodePath(selector(table)));
}
@Override
public OrderByBuilder nodeName( String table ) {
return addOrdering(new NodeName(selector(table)));
}
@Override
public OrderByBuilder nodeLocalName( String table ) {
return addOrdering(new NodeLocalName(selector(table)));
}
@Override
public OrderByOperandBuilder lowerCaseOf() {
return new SingleOrderByOperandBuilder(builder, order, nullOrder) {
@Override
protected OrderByBuilder addOrdering( DynamicOperand operand ) {
return super.addOrdering(new LowerCase(operand));
}
};
}
@Override
public OrderByOperandBuilder upperCaseOf() {
return new SingleOrderByOperandBuilder(builder, order, nullOrder) {
@Override
protected OrderByBuilder addOrdering( DynamicOperand operand ) {
return super.addOrdering(new UpperCase(operand));
}
};
}
}
/**
* Class used to specify a join clause of a query.
*
* @see QueryBuilder#join(String)
* @see QueryBuilder#innerJoin(String)
* @see QueryBuilder#leftOuterJoin(String)
* @see QueryBuilder#rightOuterJoin(String)
* @see QueryBuilder#fullOuterJoin(String)
*/
public class JoinClause {
private final NamedSelector rightSource;
private final JoinType type;
protected JoinClause( NamedSelector rightTable,
JoinType type ) {
this.rightSource = rightTable;
this.type = type;
}
/**
* Walk the current source or the 'rightSource' to find the named selector with the supplied name or alias
*
* @param tableName the table name
* @return the selector name matching the supplied table name; never null
* @throws IllegalArgumentException if the table name could not be resolved
*/
protected SelectorName nameOf( String tableName ) {
final SelectorName name = new SelectorName(tableName);
// Look at the right source ...
if (rightSource.aliasOrName().equals(name)) return name;
// Look through the left source ...
final AtomicBoolean notFound = new AtomicBoolean(true);
Visitors.visitAll(source, new Visitors.AbstractVisitor() {
@Override
public void visit( AllNodes selector ) {
if (notFound.get() && selector.aliasOrName().equals(name)) notFound.set(false);
}
@Override
public void visit( NamedSelector selector ) {
if (notFound.get() && selector.aliasOrName().equals(name)) notFound.set(false);
}
});
if (notFound.get()) {
throw new IllegalArgumentException("Expected \"" + tableName + "\" to be a valid table name or alias");
}
return name;
}
/**
* Define the join as using an equi-join criteria by specifying the expression equating two columns. Each column reference
* must be qualified with the appropriate table name or alias.
*
* @param columnEqualExpression the equality expression between the two tables; may not be null
* @return the query builder instance, for method chaining purposes
* @throws IllegalArgumentException if the supplied expression is not an equality expression
*/
public QueryBuilder on( String columnEqualExpression ) {
String[] parts = columnEqualExpression.split("=");
if (parts.length != 2) {
throw new IllegalArgumentException("Expected equality expression for columns, but found \""
+ columnEqualExpression + "\"");
}
return createJoin(new EquiJoinCondition(column(parts[0]), column(parts[1])));
}
/**
* Define the join criteria to require the two tables represent the same node. The supplied tables must be a valid name or
* alias.
*
* @param table1 the name or alias of the first table
* @param table2 the name or alias of the second table
* @return the query builder instance, for method chaining purposes
*/
public QueryBuilder onSameNode( String table1,
String table2 ) {
return createJoin(new SameNodeJoinCondition(nameOf(table1), nameOf(table2)));
}
/**
* Define the join criteria to require the node in one table is a descendant of the node in another table. The supplied
* tables must be a valid name or alias.
*
* @param ancestorTable the name or alias of the table containing the ancestor node
* @param descendantTable the name or alias of the table containing the descendant node
* @return the query builder instance, for method chaining purposes
*/
public QueryBuilder onDescendant( String ancestorTable,
String descendantTable ) {
return createJoin(new DescendantNodeJoinCondition(nameOf(ancestorTable), nameOf(descendantTable)));
}
/**
* Define the join criteria to require the node in one table is a child of the node in another table. The supplied tables
* must be a valid name or alias.
*
* @param parentTable the name or alias of the table containing the parent node
* @param childTable the name or alias of the table containing the child node
* @return the query builder instance, for method chaining purposes
*/
public QueryBuilder onChildNode( String parentTable,
String childTable ) {
return createJoin(new ChildNodeJoinCondition(nameOf(parentTable), nameOf(childTable)));
}
protected QueryBuilder createJoin( JoinCondition condition ) {
// CROSS joins have a higher precedence, so we may need to adjust the existing left side in this case...
if (type == JoinType.CROSS && source instanceof Join && ((Join)source).type() != JoinType.CROSS) {
// A CROSS join follows a non-CROSS join, so the CROSS join becomes precendent ...
Join left = (Join)source;
Join cross = new Join(left.getRight(), type, rightSource, condition);
source = new Join(left.getLeft(), left.type(), cross, left.getJoinCondition());
} else {
// Otherwise, just create using usual precedence ...
source = new Join(source, type, rightSource, condition);
}
return QueryBuilder.this;
}
}
/**
* Interface that defines a dynamic operand portion of a criteria.
*/
public interface DynamicOperandBuilder {
/**
* Constrains the nodes in the the supplied table such that they must have a property value whose length matches the
* criteria.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param property the name of the property; may not be null and must refer to a valid property name
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder length( String table,
String property );
/**
* Constrains the nodes in the the supplied table such that they must have a matching value for the named property.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param property the name of the property; may not be null and must refer to a valid property name
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder propertyValue( String table,
String property );
/**
* Constrains the nodes in the the supplied table such that they must have a matching value for any of the node's
* reference properties.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder referenceValue( String table );
/**
* Constrains the nodes in the the supplied table such that they must have a matching value for the named property.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param property the name of the reference property; may be null if the constraint applies to all/any reference
* properties on the node
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder referenceValue( String table,
String property );
/**
* Constrains the nodes in the the supplied table such that they must have a matching value for any of the node's non-weak
* reference properties.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder strongReferenceValue( String table );
/**
* Constrains the nodes in the the supplied table such that they must satisfy the supplied full-text search on the nodes'
* property values.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder fullTextSearchScore( String table );
/**
* Constrains the nodes in the the supplied table based upon criteria on the node's number of children.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder childCount( String table );
/**
* Constrains the nodes in the the supplied table based upon criteria on the node's depth.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder depth( String table );
/**
* Constrains the nodes in the the supplied table based upon criteria on the node's path.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder path( String table );
/**
* Constrains the nodes in the the supplied table based upon criteria on the node's local name.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder nodeLocalName( String table );
/**
* Constrains the nodes in the the supplied table based upon criteria on the node's name.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @return the interface for completing the value portion of the criteria specification; never null
*/
public ComparisonBuilder nodeName( String table );
/**
* Begin a constraint against the uppercase form of a dynamic operand.
*
* @return the interface for completing the criteria specification; never null
*/
public DynamicOperandBuilder upperCaseOf();
/**
* Begin a constraint against the lowercase form of a dynamic operand.
*
* @return the interface for completing the criteria specification; never null
*/
public DynamicOperandBuilder lowerCaseOf();
}
public class ConstraintBuilder implements DynamicOperandBuilder {
private final ConstraintBuilder parent;
/** Used for the current operations */
private Constraint constraint;
/** Set when a logical criteria is started */
private Constraint left;
private boolean and;
private boolean negateConstraint;
private boolean implicitParentheses = true;
protected ConstraintBuilder( ConstraintBuilder parent ) {
this.parent = parent;
}
/**
* Complete this constraint specification.
*
* @return the query builder, for method chaining purposes
*/
public QueryBuilder end() {
buildLogicalConstraint();
QueryBuilder.this.constraint = constraint;
return QueryBuilder.this;
}
/**
* Simulate the use of an open parenthesis in the constraint. The resulting builder should be used to define the
* constraint within the parenthesis, and should always be terminated with a {@link #closeParen()}.
*
* @return the constraint builder that should be used to define the portion of the constraint within the parenthesis;
* never null
* @see #closeParen()
*/
public ConstraintBuilder openParen() {
return new ConstraintBuilder(this);
}
/**
* Complete the specification of a constraint clause, and return the builder for the parent constraint clause.
*
* @return the constraint builder that was used to create this parenthetical constraint clause builder; never null
* @throws IllegalStateException if there was not an {@link #openParen() open parenthesis} to close
*/
public ConstraintBuilder closeParen() {
if (parent == null) {
throw new IllegalStateException(GraphI18n.unexpectedClosingParenthesis.text());
}
buildLogicalConstraint();
parent.implicitParentheses = false;
return parent.setConstraint(constraint);
}
/**
* Signal that the previous constraint clause be AND-ed together with another constraint clause that will be defined
* immediately after this method call.
*
* @return the constraint builder for the remaining constraint clause; never null
*/
public ConstraintBuilder and() {
buildLogicalConstraint();
left = constraint;
constraint = null;
and = true;
return this;
}
/**
* Signal that the previous constraint clause be OR-ed together with another constraint clause that will be defined
* immediately after this method call.
*
* @return the constraint builder for the remaining constraint clause; never null
*/
public ConstraintBuilder or() {
buildLogicalConstraint();
left = constraint;
constraint = null;
and = false;
return this;
}
/**
* Signal that the next constraint clause (defined immediately after this method) should be negated.
*
* @return the constraint builder for the constraint clause that is to be negated; never null
*/
public ConstraintBuilder not() {
negateConstraint = true;
return this;
}
protected ConstraintBuilder buildLogicalConstraint() {
if (negateConstraint && constraint != null) {
constraint = new Not(constraint);
negateConstraint = false;
}
if (left != null && constraint != null) {
if (and) {
// If the left constraint is an OR, we need to rearrange things since AND is higher precedence ...
if (left instanceof Or && implicitParentheses) {
Or previous = (Or)left;
constraint = new Or(previous.left(), new And(previous.right(), constraint));
} else {
constraint = new And(left, constraint);
}
} else {
constraint = new Or(left, constraint);
}
left = null;
}
return this;
}
/**
* Define a constraint clause that the node within the named table is the same node as that appearing at the supplied
* path.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param asNodeAtPath the path to the node
* @return the constraint builder that was used to create this clause; never null
*/
public ConstraintBuilder isSameNode( String table,
String asNodeAtPath ) {
return setConstraint(new SameNode(selector(table), asNodeAtPath));
}
/**
* Define a constraint clause that the node within the named table is the child of the node at the supplied path.
*
* @param childTable the name of the table; may not be null and must refer to a valid name or alias of a table appearing
* in the FROM clause
* @param parentPath the path to the parent node
* @return the constraint builder that was used to create this clause; never null
*/
public ConstraintBuilder isChild( String childTable,
String parentPath ) {
return setConstraint(new ChildNode(selector(childTable), parentPath));
}
/**
* Define a constraint clause that the node within the named table is a descendant of the node at the supplied path.
*
* @param descendantTable the name of the table; may not be null and must refer to a valid name or alias of a table
* appearing in the FROM clause
* @param ancestorPath the path to the ancestor node
* @return the constraint builder that was used to create this clause; never null
*/
public ConstraintBuilder isBelowPath( String descendantTable,
String ancestorPath ) {
return setConstraint(new DescendantNode(selector(descendantTable), ancestorPath));
}
/**
* Define a constraint clause that the node within the named table has at least one value for the named property.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param propertyName the name of the property
* @return the constraint builder that was used to create this clause; never null
*/
public ConstraintBuilder hasProperty( String table,
String propertyName ) {
return setConstraint(new PropertyExistence(selector(table), propertyName));
}
/**
* Define a constraint clause that the node within the named table have at least one property that satisfies the full-text
* search expression.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param searchExpression the full-text search expression
* @return the constraint builder that was used to create this clause; never null
*/
public ConstraintBuilder search( String table,
String searchExpression ) {
return setConstraint(new FullTextSearch(selector(table), searchExpression));
}
/**
* Define a constraint clause that the node within the named table have a value for the named property that satisfies the
* full-text search expression.
*
* @param table the name of the table; may not be null and must refer to a valid name or alias of a table appearing in the
* FROM clause
* @param propertyName the name of the property to be searched
* @param searchExpression the full-text search expression
* @return the constraint builder that was used to create this clause; never null
*/
public ConstraintBuilder search( String table,
String propertyName,
String searchExpression ) {
return setConstraint(new FullTextSearch(selector(table), propertyName, searchExpression));
}
protected ComparisonBuilder comparisonBuilder( DynamicOperand operand ) {
return new ComparisonBuilder(this, operand);
}
@Override
public ComparisonBuilder length( String table,
String property ) {
return comparisonBuilder(new Length(new PropertyValue(selector(table), property)));
}
@Override
public ComparisonBuilder propertyValue( String table,
String property ) {
return comparisonBuilder(new PropertyValue(selector(table), property));
}
@Override
public ComparisonBuilder strongReferenceValue( String table ) {
return comparisonBuilder(new ReferenceValue(selector(table), null, false, false));
}
@Override
public ComparisonBuilder referenceValue( String table ) {
return comparisonBuilder(new ReferenceValue(selector(table)));
}
@Override
public ComparisonBuilder referenceValue( String table,
String property ) {
return comparisonBuilder(new ReferenceValue(selector(table), property));
}
@Override
public ComparisonBuilder fullTextSearchScore( String table ) {
return comparisonBuilder(new FullTextSearchScore(selector(table)));
}
@Override
public ComparisonBuilder childCount( String table ) {
return comparisonBuilder(new ChildCount(selector(table)));
}
@Override
public ComparisonBuilder depth( String table ) {
return comparisonBuilder(new NodeDepth(selector(table)));
}
@Override
public ComparisonBuilder path( String table ) {
return comparisonBuilder(new NodePath(selector(table)));
}
@Override
public ComparisonBuilder nodeLocalName( String table ) {
return comparisonBuilder(new NodeLocalName(selector(table)));
}
@Override
public ComparisonBuilder nodeName( String table ) {
return comparisonBuilder(new NodeName(selector(table)));
}
@Override
public DynamicOperandBuilder upperCaseOf() {
return new UpperCaser(this);
}
@Override
public DynamicOperandBuilder lowerCaseOf() {
return new LowerCaser(this);
}
protected ConstraintBuilder setConstraint( Constraint constraint ) {
if (this.constraint != null && this.left == null) {
and();
}
this.constraint = constraint;
return buildLogicalConstraint();
}
}
/**
* A specialized form of the {@link ConstraintBuilder} that always wraps the generated constraint in a {@link UpperCase}
* instance.
*/
protected class UpperCaser extends ConstraintBuilder {
private final ConstraintBuilder delegate;
protected UpperCaser( ConstraintBuilder delegate ) {
super(null);
this.delegate = delegate;
}
@Override
protected ConstraintBuilder setConstraint( Constraint constraint ) {
Comparison comparison = (Comparison)constraint;
return delegate.setConstraint(new Comparison(new UpperCase(comparison.getOperand1()), comparison.operator(),
comparison.getOperand2()));
}
}
/**
* A specialized form of the {@link ConstraintBuilder} that always wraps the generated constraint in a {@link LowerCase}
* instance.
*/
protected class LowerCaser extends ConstraintBuilder {
private final ConstraintBuilder delegate;
protected LowerCaser( ConstraintBuilder delegate ) {
super(null);
this.delegate = delegate;
}
@Override
protected ConstraintBuilder setConstraint( Constraint constraint ) {
Comparison comparison = (Comparison)constraint;
return delegate.setConstraint(new Comparison(new LowerCase(comparison.getOperand1()), comparison.operator(),
comparison.getOperand2()));
}
}
public abstract class CastAs© 2015 - 2024 Weber Informatics LLC | Privacy Policy