/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.cayenne.exp;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.exp.parser.ASTPath;
import org.apache.cayenne.query.Ordering;
import org.apache.cayenne.query.PrefetchTreeNode;
import org.apache.cayenne.query.SortOrder;
import org.apache.cayenne.reflect.PropertyUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* <p>
* A property in a {@link org.apache.cayenne.DataObject}.
* </p>
* <p>
* Used to construct Expressions quickly and with type-safety, and to construct Orderings.
* </p>
* <p>
* Instances of this class are immutable.
* </p>
* <p>
* Must be created via factory methods {@link Property#create(String, Class) Property.create(..)}
* </p>
*
* @param <E> The type this property returns.
*
* @see Property#create(String, Class)
* @see Property#create(Expression, Class)
* @see Property#create(String, Expression, Class)
*
* @since 4.0
*/
public class Property<E> {
/**
* <p>Property that can be used in COUNT(*) queries</p>
* <p>
* <pre>{@code
* List<Object[]> result = ObjectSelect
* .columnQuery(Artist.class, Property.COUNT, Artist.ARTIST_NAME)
* .having(Property.COUNT.gt(1L))
* .select(context);
* }</pre>
* </p>
*/
public static final Property<Long> COUNT = Property.create(FunctionExpressionFactory.countExp(), Long.class);
/**
* Name of the property in the object
*/
private final String name;
/**
* Expression provider for the property
*/
private final ExpressionProvider expressionProvider;
/**
* Explicit type of the property
*/
private final Class<? super E> type;
/**
* Constructs a new property with the given name.
*
* @param name name of the property (usually it's obj path)
*
* @see Property#create(String, Class)
* @deprecated use factory method Property.create("propertyName", PropertyType.class)
*/
public Property(final String name) {
this(name, null);
}
/**
* Constructs a new property with the given name and type.
*
* @param name of the property (usually it's obj path)
* @param type of the property
*
* @see Property#create(String, Class)
*/
protected Property(final String name, final Class<? super E> type) {
this.name = name;
expressionProvider = new ExpressionProvider() {
@Override
public Expression get() {
return ExpressionFactory.pathExp(name);
}
};
this.type = type;
}
/**
* Constructs a new property with the given name and expression
*
* @param name of the property (will be used as alias for the expression)
* @param expression expression for property
* @param type of the property
*
* @see Property#create(String, Expression, Class)
*/
protected Property(final String name, final Expression expression, final Class<? super E> type) {
this.name = name;
expressionProvider = new ExpressionProvider() {
@Override
public Expression get() {
return expression.deepCopy();
}
};
this.type = type;
}
/**
* @return Name of the property in the object.
*/
public String getName() {
return name;
}
/**
* @return alias for this property
*/
public String getAlias() {
if(getName() == null) {
return null;
}
// check if default name for Path expression is overridden
Expression exp = getExpression();
if(exp instanceof ASTPath) {
if(((ASTPath) exp).getPath().equals(getName())) {
return null;
}
}
return getName();
}
/**
* @return expression that represents this Property
*/
public Expression getExpression() {
return expressionProvider.get();
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : expressionProvider.get().hashCode();
if(type != null) {
result = 31 * result + type.hashCode();
}
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Property<?> property = (Property<?>) o;
if (name != null ? !name.equals(property.name) : property.name != null) return false;
if (name == null && !expressionProvider.get().equals(property.expressionProvider.get())) return false;
return (type == null ? property.type == null : type.equals(property.type));
}
/**
* Constructs a property path by appending the argument to the existing property separated by a dot.
*
* @return a newly created Property object.
*/
public Property<Object> dot(String property) {
return create(getName() + "." + property, null);
}
/**
* Constructs a new property path by appending the argument to the existing property separated by a dot.
*
* @return a newly created Property object.
*/
public <T> Property<T> dot(Property<T> property) {
return create(getName() + "." + property.getName(), property.getType());
}
/**
* Returns a version of this property that represents an OUTER join. It is
* up to caller to ensure that the property corresponds to a relationship,
* as "outer" attributes make no sense.
*/
public Property<E> outer() {
return isOuter() ? this : create(name + "+", type);
}
private boolean isOuter() {
return name.endsWith("+");
}
/**
* Converts this property to a path expression.
* This method is equivalent of getExpression() which is preferred as more generic.
*
* @return a newly created expression.
* @see Property#getExpression()
*/
public Expression path() {
return getExpression();
}
/**
* @return An expression representing null.
*/
public Expression isNull() {
return ExpressionFactory.matchExp(getExpression(), null);
}
/**
* @return An expression representing a non-null value.
*/
public Expression isNotNull() {
return ExpressionFactory.matchExp(getExpression(), null).notExp();
}
/**
* @return An expression representing equality to TRUE.
*/
public Expression isTrue() {
return ExpressionFactory.matchExp(getExpression(), Boolean.TRUE);
}
/**
* @return An expression representing equality to FALSE.
*/
public Expression isFalse() {
return ExpressionFactory.matchExp(getExpression(), Boolean.FALSE);
}
/**
* @return An expression representing equality to a value.
*/
public Expression eq(E value) {
return ExpressionFactory.matchExp(getExpression(), value);
}
/**
* @return An expression representing equality between two attributes
* (columns).
*/
public Expression eq(Property<?> value) {
return ExpressionFactory.matchExp(getExpression(), value.getExpression());
}
/**
* @return An expression representing inequality to a value.
*/
public Expression ne(E value) {
return ExpressionFactory.noMatchExp(getExpression(), value);
}
/**
* @return An expression representing inequality between two attributes
* (columns).
*/
public Expression ne(Property<?> value) {
return ExpressionFactory.noMatchExp(getExpression(), value.getExpression());
}
/**
* @param pattern a pattern matching property value. Pattern may include "_" and
* "%" wildcard symbols to match any single character or a
* sequence of characters. To prevent "_" and "%" from being
* treated as wildcards, they need to be escaped and escape char
* passed with {@link #like(String, char)} method.
* @return An expression for a Database "LIKE" query.
*/
public Expression like(String pattern) {
return ExpressionFactory.likeExp(getExpression(), pattern);
}
/**
* @param pattern a properly escaped pattern matching property value. Pattern
* may include "_" and "%" wildcard symbols to match any single
* character or a sequence of characters.
* @param escapeChar an escape character used in the pattern to escape "%" and "_".
* @return An expression for a Database "LIKE" query.
*/
public Expression like(String pattern, char escapeChar) {
return ExpressionFactory.likeExp(getExpression(), pattern, escapeChar);
}
/**
* @return An expression for a case insensitive "LIKE" query.
*/
public Expression likeIgnoreCase(String pattern) {
return ExpressionFactory.likeIgnoreCaseExp(getExpression(), pattern);
}
/**
* @return An expression for a Database "NOT LIKE" query.
*/
public Expression nlike(String value) {
return ExpressionFactory.notLikeExp(getExpression(), value);
}
/**
* @return An expression for a case insensitive "NOT LIKE" query.
*/
public Expression nlikeIgnoreCase(String value) {
return ExpressionFactory.notLikeIgnoreCaseExp(getExpression(), value);
}
/**
* Creates an expression for a database "LIKE" query with the value converted to a pattern matching anywhere in the
* String.
*
* @param substring a String to match against property value. "_" and "%" symbols
* are NOT treated as wildcards and are escaped when converted to
* a LIKE expression.
* @return a newly created expression.
*/
public Expression contains(String substring) {
return ExpressionFactory.containsExp(getExpression(), substring);
}
/**
* Creates an expression for a database "LIKE" query with the value converted to a pattern matching the beginning of
* a String.
*
* @param value a String to match against property value. "_" and "%" symbols
* are NOT treated as wildcards and are escaped when converted to
* a LIKE expression.
* @return a newly created expression.
*/
public Expression startsWith(String value) {
return ExpressionFactory.startsWithExp(getExpression(), value);
}
/**
* Creates an expression for a database "LIKE" query with the value
* converted to a pattern matching the tail of a String.
*
* @param value a String to match against property value. "_" and "%" symbols
* are NOT treated as wildcards and are escaped when converted to
* a LIKE expression.
* @return a newly created expression.
*/
public Expression endsWith(String value) {
return ExpressionFactory.endsWithExp(getExpression(), value);
}
/**
* Same as {@link #contains(String)}, only using case-insensitive
* comparison.
*/
public Expression containsIgnoreCase(String value) {
return ExpressionFactory.containsIgnoreCaseExp(getExpression(), value);
}
/**
* Same as {@link #startsWith(String)}, only using case-insensitive
* comparison.
*/
public Expression startsWithIgnoreCase(String value) {
return ExpressionFactory.startsWithIgnoreCaseExp(getExpression(), value);
}
/**
* Same as {@link #endsWith(String)}, only using case-insensitive
* comparison.
*/
public Expression endsWithIgnoreCase(String value) {
return ExpressionFactory.endsWithIgnoreCaseExp(getExpression(), value);
}
/**
* @param lower The lower bound.
* @param upper The upper bound.
* @return An expression checking for objects between a lower and upper
* bound inclusive
*/
public Expression between(E lower, E upper) {
return ExpressionFactory.betweenExp(getExpression(), lower, upper);
}
/**
* @return An expression for finding objects with values in the given set.
*/
public Expression in(E firstValue, E... moreValues) {
int moreValuesLength = moreValues != null ? moreValues.length : 0;
Object[] values = new Object[moreValuesLength + 1];
values[0] = firstValue;
if (moreValuesLength > 0) {
System.arraycopy(moreValues, 0, values, 1, moreValuesLength);
}
return ExpressionFactory.inExp(getExpression(), values);
}
/**
* @return An expression for finding objects with values not in the given
* set.
*/
public Expression nin(E firstValue, E... moreValues) {
int moreValuesLength = moreValues != null ? moreValues.length : 0;
Object[] values = new Object[moreValuesLength + 1];
values[0] = firstValue;
if (moreValuesLength > 0) {
System.arraycopy(moreValues, 0, values, 1, moreValuesLength);
}
return ExpressionFactory.notInExp(getExpression(), values);
}
/**
* @return An expression for finding objects with values in the given set.
*/
public Expression in(Collection<E> values) {
return ExpressionFactory.inExp(getExpression(), values);
}
/**
* @return An expression for finding objects with values not in the given
* set.
*/
public Expression nin(Collection<E> values) {
return ExpressionFactory.notInExp(getExpression(), values);
}
/**
* @return A greater than Expression.
*/
public Expression gt(E value) {
return ExpressionFactory.greaterExp(getExpression(), value);
}
/**
* @return Represents a greater than relationship between two attributes
* (columns).
*/
public Expression gt(Property<?> value) {
return ExpressionFactory.greaterExp(getExpression(), value.getExpression());
}
/**
* @return A greater than or equal to Expression.
*/
public Expression gte(E value) {
return ExpressionFactory.greaterOrEqualExp(getExpression(), value);
}
/**
* @return Represents a greater than or equal relationship between two
* attributes (columns).
*/
public Expression gte(Property<?> value) {
return ExpressionFactory.greaterOrEqualExp(getExpression(), value.getExpression());
}
/**
* @return A less than Expression.
*/
public Expression lt(E value) {
return ExpressionFactory.lessExp(getExpression(), value);
}
/**
* @return Represents a less than relationship between two attributes
* (columns).
*/
public Expression lt(Property<?> value) {
return ExpressionFactory.lessExp(getExpression(), value.getExpression());
}
/**
* @return A less than or equal to Expression.
*/
public Expression lte(E value) {
return ExpressionFactory.lessOrEqualExp(getExpression(), value);
}
/**
* @return Represents a less than or equal relationship between two
* attributes (columns).
*/
public Expression lte(Property<?> value) {
return ExpressionFactory.lessOrEqualExp(getExpression(), value.getExpression());
}
/**
* @return Ascending sort orderings on this property.
*/
public Ordering asc() {
return new Ordering(getExpression(), SortOrder.ASCENDING);
}
/**
* @return Ascending sort orderings on this property.
*/
public List<Ordering> ascs() {
List<Ordering> result = new ArrayList<>(1);
result.add(asc());
return result;
}
/**
* @return Ascending case insensitive sort orderings on this property.
*/
public Ordering ascInsensitive() {
return new Ordering(getExpression(), SortOrder.ASCENDING_INSENSITIVE);
}
/**
* @return Ascending case insensitive sort orderings on this property.
*/
public List<Ordering> ascInsensitives() {
List<Ordering> result = new ArrayList<>(1);
result.add(ascInsensitive());
return result;
}
/**
* @return Descending sort orderings on this property.
*/
public Ordering desc() {
return new Ordering(getExpression(), SortOrder.DESCENDING);
}
/**
* @return Descending sort orderings on this property.
*/
public List<Ordering> descs() {
List<Ordering> result = new ArrayList<>(1);
result.add(desc());
return result;
}
/**
* @return Descending case insensitive sort orderings on this property.
*/
public Ordering descInsensitive() {
return new Ordering(getExpression(), SortOrder.DESCENDING_INSENSITIVE);
}
/**
* @return Descending case insensitive sort orderings on this property.
*/
public List<Ordering> descInsensitives() {
List<Ordering> result = new ArrayList<>(1);
result.add(descInsensitive());
return result;
}
/**
* Returns a prefetch tree that follows this property path, potentially
* spanning a number of phantom nodes, and having a single leaf with "joint"
* prefetch semantics.
*/
public PrefetchTreeNode joint() {
return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
}
/**
* Returns a prefetch tree that follows this property path, potentially
* spanning a number of phantom nodes, and having a single leaf with
* "disjoint" prefetch semantics.
*/
public PrefetchTreeNode disjoint() {
return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
}
/**
* Returns a prefetch tree that follows this property path, potentially
* spanning a number of phantom nodes, and having a single leaf with
* "disjoint by id" prefetch semantics.
*/
public PrefetchTreeNode disjointById() {
return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
}
/**
* Extracts property value from an object using JavaBean-compatible
* introspection with one addition - a property can be a dot-separated
* property name path.
*/
@SuppressWarnings("unchecked")
public E getFrom(Object bean) {
return (E) PropertyUtils.getProperty(bean, getName());
}
/**
* Extracts property value from a collection of objects using
* JavaBean-compatible introspection with one addition - a property can be a
* dot-separated property name path.
*/
public List<E> getFromAll(Collection<?> beans) {
List<E> result = new ArrayList<>(beans.size());
for (Object bean : beans) {
result.add(getFrom(bean));
}
return result;
}
/**
* Sets a property value in 'obj' using JavaBean-compatible introspection
* with one addition - a property can be a dot-separated property name path.
*/
public void setIn(Object bean, E value) {
PropertyUtils.setProperty(bean, getName(), value);
}
/**
* Sets a property value in a collection of objects using
* JavaBean-compatible introspection with one addition - a property can be a
* dot-separated property name path.
*/
public void setInAll(Collection<?> beans, E value) {
for (Object bean : beans) {
setIn(bean, value);
}
}
/**
* @see FunctionExpressionFactory#countExp(Expression)
*/
public Property<Long> count() {
return create(FunctionExpressionFactory.countExp(getExpression()), Long.class);
}
/**
* @see FunctionExpressionFactory#maxExp(Expression)
*/
public Property<E> max() {
return create(FunctionExpressionFactory.maxExp(getExpression()), getType());
}
/**
* @see FunctionExpressionFactory#minExp(Expression)
*/
public Property<E> min() {
return create(FunctionExpressionFactory.minExp(getExpression()), getType());
}
/**
* @see FunctionExpressionFactory#avgExp(Expression)
*/
public Property<E> avg() {
return create(FunctionExpressionFactory.avgExp(getExpression()), getType());
}
/**
* @see FunctionExpressionFactory#sumExp(Expression)
*/
public Property<E> sum() {
return create(FunctionExpressionFactory.sumExp(getExpression()), getType());
}
/**
* @see FunctionExpressionFactory#modExp(Expression, Number)
*/
public Property<E> mod(Number number) {
return create(FunctionExpressionFactory.modExp(getExpression(), number), getType());
}
/**
* @see FunctionExpressionFactory#absExp(Expression)
*/
public Property<E> abs() {
return create(FunctionExpressionFactory.absExp(getExpression()), getType());
}
/**
* @see FunctionExpressionFactory#sqrtExp(Expression)
*/
public Property<E> sqrt() {
return create(FunctionExpressionFactory.sqrtExp(getExpression()), getType());
}
/**
* @see FunctionExpressionFactory#lengthExp(Expression)
*/
public Property<Integer> length() {
return create(FunctionExpressionFactory.lengthExp(getExpression()), Integer.class);
}
/**
* @see FunctionExpressionFactory#locateExp(String, Expression)
*/
public Property<Integer> locate(String string) {
return create(FunctionExpressionFactory.locateExp(ExpressionFactory.wrapScalarValue(string), getExpression()), Integer.class);
}
/**
* @see FunctionExpressionFactory#locateExp(Expression, Expression)
*/
public Property<Integer> locate(Property<? extends String> property) {
return create(FunctionExpressionFactory.locateExp(property.getExpression(), getExpression()), Integer.class);
}
/**
* @see FunctionExpressionFactory#trimExp(Expression)
*/
public Property<String> trim() {
return create(FunctionExpressionFactory.trimExp(getExpression()), String.class);
}
/**
* @see FunctionExpressionFactory#upperExp(Expression)
*/
public Property<String> upper() {
return create(FunctionExpressionFactory.upperExp(getExpression()), String.class);
}
/**
* @see FunctionExpressionFactory#lowerExp(Expression)
*/
public Property<String> lower() {
return create(FunctionExpressionFactory.lowerExp(getExpression()), String.class);
}
/**
* <p>Arguments will be converted as follows:
* <ul>
* <li>if argument is a {@link Property} than its expression will be used</li>
* <li>if argument is a {@link Expression} than it will be used as is </li>
* <li>all other values will be converted to String</li>
* </ul>
* </p>
* <p>
* Usage:
* <pre>{@code
* Property<String> fullName = Artist.FIRST_NAME.concat(" ", Artist.SECOND_NAME);
* }</pre>
* </p>
* @see FunctionExpressionFactory#concatExp(Expression...)
*/
public Property<String> concat(Object... args) {
Expression[] exp = new Expression[args.length + 1];
int i = 0;
exp[i++] = getExpression();
for(Object arg : args) {
if(arg instanceof Property) {
exp[i++] = ((Property) arg).getExpression();
} else if(arg instanceof Expression) {
exp[i++] = (Expression) arg;
} else if(arg != null) {
exp[i++] = ExpressionFactory.wrapScalarValue(arg.toString());
}
}
return create(FunctionExpressionFactory.concatExp(exp), String.class);
}
/**
* @see FunctionExpressionFactory#substringExp(Expression, int, int)
*/
public Property<String> substring(int offset, int length) {
return create(FunctionExpressionFactory.substringExp(getExpression(), offset, length), String.class);
}
/**
* Creates alias with different name for this property
*/
public Property<E> alias(String alias) {
return new Property<>(alias, this.getExpression(), this.getType());
}
/**
* <p>Create new "flat" property for toMany relationship.</p>
* <p>
* Example:
* <pre>{@code
* List<Object[]> result = ObjectSelect
* .columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.PAINTING_ARRAY.flat(Painting.class))
* .select(context);
* }</pre>
* </p>
*/
public <T extends Persistent> Property<T> flat(Class<? super T> tClass) {
if(!Collection.class.isAssignableFrom(type) && !Map.class.isAssignableFrom(type)) {
throw new CayenneRuntimeException("Can use flat() function only on Property mapped on toMany relationship.");
}
return create(ExpressionFactory.fullObjectExp(getExpression()), tClass);
}
public Class<? super E> getType() {
return type;
}
/**
* Creates property with name and type
* @see Property#create(Expression, Class)
* @see Property#create(String, Expression, Class)
*/
public static <T> Property<T> create(String name, Class<? super T> type) {
return new Property<>(name, type);
}
/**
* Creates property with expression and type
* @see Property#create(String, Class)
* @see Property#create(String, Expression, Class)
*/
public static <T> Property<T> create(Expression expression, Class<? super T> type) {
return new Property<>(null, expression, type);
}
/**
* Creates property with name, expression and type
* @see Property#create(String, Class)
* @see Property#create(Expression, Class)
*/
public static <T> Property<T> create(String name, Expression expression, Class<? super T> type) {
return new Property<>(name, expression, type);
}
/**
* <p>
* Creates "self" Property for persistent class.
* This property can be used to select full object along with some of it properties (or
* properties that can be resolved against query root)
* </p>
* <p>
* Here is sample code, that will select all Artists and count of their Paintings:
* <pre>{@code
* Property<Artist> artistFull = Property.createSelf(Artist.class);
* List<Object[]> result = ObjectSelect
* .columnQuery(Artist.class, artistFull, Artist.PAINTING_ARRAY.count())
* .select(context);
* }
* </pre>
* </p>
*/
public static <T extends Persistent> Property<T> createSelf(Class<? super T> type) {
return new Property<>(null, ExpressionFactory.fullObjectExp(), type);
}
/**
* Since Expression is mutable we need to provide clean Expression for every getter call.
* So to keep Property itself immutable we use ExpressionProvider.
* @see Property#Property(String, Class)
* @see Property#Property(String, Expression, Class)
*/
private interface ExpressionProvider {
Expression get();
}
}