Version 1.7.4
[gae.git] / java / src / main / com / google / appengine / api / datastore / Query.java
blobb0c81a4239de13cd867d211b8d1d8991e745f059
1 // Copyright 2007 Google Inc. All rights reserved.
3 package com.google.appengine.api.datastore;
5 import static com.google.common.base.Preconditions.checkNotNull;
7 import com.google.appengine.api.NamespaceManager;
8 import com.google.common.base.Joiner;
9 import com.google.common.base.Objects;
10 import com.google.common.base.Preconditions;
11 import com.google.common.collect.ImmutableList;
12 import com.google.common.collect.Lists;
13 import com.google.common.collect.Maps;
15 import java.io.IOException;
16 import java.io.ObjectInputStream;
17 import java.io.Serializable;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.Map;
24 /**
25 * {@link Query} encapsulates a request for zero or more {@link Entity} objects
26 * out of the datastore. It supports querying on zero or more properties,
27 * querying by ancestor, and sorting. {@link Entity} objects which match the
28 * query can be retrieved in a single list, or with an unbounded iterator.
31 public final class Query implements Serializable {
33 @Deprecated
34 public static final String KIND_METADATA_KIND = Entities.KIND_METADATA_KIND;
36 @Deprecated
37 public static final String PROPERTY_METADATA_KIND = Entities.PROPERTY_METADATA_KIND;
39 @Deprecated
40 public static final String NAMESPACE_METADATA_KIND = Entities.NAMESPACE_METADATA_KIND;
42 static final long serialVersionUID = 7090652715949085374L;
44 /**
45 * SortDirection controls the order of a sort.
47 public enum SortDirection {
48 ASCENDING,
49 DESCENDING
52 /**
53 * Operators supported by {@link FilterPredicate}.
55 public enum FilterOperator {
56 LESS_THAN("<"),
57 LESS_THAN_OR_EQUAL("<="),
58 GREATER_THAN(">"),
59 GREATER_THAN_OR_EQUAL(">="),
60 EQUAL("="),
61 NOT_EQUAL("!="),
62 IN("IN");
64 private final String shortName;
65 private FilterOperator(String shortName) {
66 this.shortName = shortName;
69 @Override
70 public String toString() {
71 return shortName;
74 public FilterPredicate of(String propertyName, Object value) {
75 return new FilterPredicate(propertyName, this, value);
79 /**
80 * Operators supported by {@link CompositeFilter}.
82 public enum CompositeFilterOperator {
83 AND,
84 OR;
86 public static CompositeFilter and(Filter... subFilters) {
87 return and(Arrays.asList(subFilters));
90 public static CompositeFilter and(Collection<Filter> subFilters) {
91 return new CompositeFilter(AND, subFilters);
94 public static CompositeFilter or(Filter... subFilters) {
95 return or(Arrays.asList(subFilters));
98 public static CompositeFilter or(Collection<Filter> subFilters) {
99 return new CompositeFilter(OR, subFilters);
102 public CompositeFilter of(Filter... subFilters) {
103 return new CompositeFilter(this, Arrays.asList(subFilters));
106 public CompositeFilter of(Collection<Filter> subFilters) {
107 return new CompositeFilter(this, subFilters);
111 private final String kind;
112 private final List<SortPredicate> sortPredicates = Lists.newArrayList();
113 private final List<FilterPredicate> filterPredicates = Lists.newArrayList();
114 private Filter filter;
115 private Key ancestor;
116 private boolean keysOnly;
117 private final Map<String, Projection> projectionMap = Maps.newHashMap();
118 private boolean distinct;
119 private AppIdNamespace appIdNamespace;
121 private transient String fullTextSearch;
124 * Create a new kindless {@link Query} that finds {@link Entity} objects.
125 * Note that kindless queries are not yet supported in the Java dev
126 * appserver.
128 * Currently the only operations supported on a kindless query are filter by
129 * __key__, ancestor, and order by __key__ ascending.
131 public Query() {
132 this(null, null);
136 * Create a new {@link Query} that finds {@link Entity} objects with
137 * the specified {@code kind}. Note that kindless queries are not yet
138 * supported in the Java dev appserver.
140 * @param kind the kind or null to create a kindless query
142 public Query(String kind) {
143 this(kind, null);
147 * Copy constructor that performs a deep copy of the provided Query.
149 * @param query The Query to copy.
151 Query(Query query) {
152 this(query.kind, query.ancestor, query.sortPredicates, query.filter, query.filterPredicates,
153 query.keysOnly, query.appIdNamespace, query.projectionMap.values(), query.distinct,
154 query.fullTextSearch);
157 Query(String kind, Key ancestor, Filter filter, boolean keysOnly, AppIdNamespace appIdNamespace,
158 boolean distinct, String fullTextSearch) {
159 this.kind = kind;
160 this.keysOnly = keysOnly;
161 this.appIdNamespace = appIdNamespace;
162 this.fullTextSearch = fullTextSearch;
163 this.distinct = distinct;
164 this.filter = filter;
165 if (ancestor != null) {
166 setAncestor(ancestor);
170 Query(String kind, Key ancestor, Collection<SortPredicate> sortPreds,
171 Filter filter,
172 Collection<FilterPredicate> filterPreds, boolean keysOnly, AppIdNamespace appIdNamespace,
173 Collection<Projection> projections, boolean distinct, String fullTextSearch) {
174 this(kind, ancestor, filter, keysOnly, appIdNamespace, distinct, fullTextSearch);
175 this.sortPredicates.addAll(sortPreds);
176 this.filterPredicates.addAll(filterPreds);
177 for (Projection projection : projections) {
178 addProjection(projection);
182 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
183 in.defaultReadObject();
184 if (appIdNamespace == null) {
185 if (ancestor != null) {
186 appIdNamespace = ancestor.getAppIdNamespace();
187 } else {
188 appIdNamespace = new AppIdNamespace(DatastoreApiHelper.getCurrentAppId(), "");
194 * Create a new {@link Query} that finds {@link Entity} objects with
195 * the specified {@code Key} as an ancestor.
197 * @param ancestor the ancestor key or null
198 * @throws IllegalArgumentException If ancestor is not complete.
200 public Query(Key ancestor) {
201 this(null, ancestor);
205 * Create a new {@link Query} that finds {@link Entity} objects with
206 * the specified {@code kind} and the specified {@code ancestor}. Note that
207 * kindless queries are not yet supported in the Java dev appserver.
209 * @param kind the kind or null to create a kindless query
210 * @param ancestor the ancestor key or null
211 * @throws IllegalArgumentException If the ancestor is not complete.
213 public Query(String kind, Key ancestor) {
214 this(kind, ancestor, null, false, DatastoreApiHelper.getCurrentAppIdNamespace(), false, null);
218 * Only {@link Entity} objects whose kind matches this value will be
219 * returned.
221 public String getKind() {
222 return kind;
226 * Returns the AppIdNamespace that is being queried.
227 * <p>The AppIdNamespace is set at construction time of this
228 * object using the {@link NamespaceManager} to retrieve the
229 * current namespace.
231 AppIdNamespace getAppIdNamespace() {
232 return appIdNamespace;
236 * Gets the current ancestor for this query, or null if there is no
237 * ancestor specified.
239 public Key getAncestor() {
240 return ancestor;
244 * Sets an ancestor for this query.
246 * This restricts the query to only return result entities that are
247 * descended from a given entity. In other words, all of the results
248 * will have the ancestor as their parent, or parent's parent, or
249 * etc.
251 * If null is specified, unsets any previously-set ancestor. Passing
252 * {@code null} as a parameter does not query for entities without
253 * ancestors (this type of query is not currently supported).
255 * @return {@code this} (for chaining)
257 * @throws IllegalArgumentException If the ancestor key is incomplete, or if
258 * you try to unset an ancestor and have not set a kind, or if you try to
259 * unset an ancestor and have not previously set an ancestor.
261 public Query setAncestor(Key ancestor) {
262 if (ancestor != null && !ancestor.isComplete()) {
263 throw new IllegalArgumentException(ancestor + " is incomplete.");
264 } else if (ancestor == null) {
265 if (this.ancestor == null) {
266 throw new IllegalArgumentException(
267 "Cannot clear ancestor unless ancestor has already been set");
270 if (ancestor != null) {
271 if (!ancestor.getAppIdNamespace().equals(appIdNamespace)) {
272 throw new IllegalArgumentException(
273 "Namespace of ancestor key and query must match.");
276 this.ancestor = ancestor;
277 return this;
281 * @param distinct if this query should be distinct. This may only be used
282 * when the query has a projection.
283 * @return {@code this} (for chaining)
285 public Query setDistinct(boolean distinct) {
286 this.distinct = distinct;
287 return this;
291 * @return if this query is distinct
292 * @see #setDistinct(boolean)
294 public boolean getDistinct() {
295 return distinct;
299 * @param filter the filter to use for this query, or {@code null}
300 * @return {@code this} (for chaining)
301 * @see CompositeFilter
302 * @see FilterPredicate
304 public Query setFilter(Filter filter) {
305 this.filter = filter;
306 return this;
310 * @return the filter for this query or {@code null}
311 * @see #setFilter(Filter)
313 public Filter getFilter() {
314 return filter;
318 * Add a {@link FilterPredicate} on the specified property.
320 * <p>All {@link FilterPredicate}s added using this message are combined using
321 * {@link CompositeFilterOperator#AND}.
323 * <p>Cannot be used in conjunction with {@link #setFilter(Filter)} which sets
324 * a single {@link Filter} instead of many {@link FilterPredicate}s.
326 * @param propertyName The name of the property to which the filter applies.
327 * @param operator The filter operator.
328 * @param value An instance of a supported datastore type. Note that
329 * entities with multi-value properties identified by {@code propertyName}
330 * will match this filter if the multi-value property has at least one
331 * value that matches the condition expressed by {@code operator} and
332 * {@code value}. For more information on multi-value property filtering
333 * please see the
334 * <a href="http://code.google.com/appengine/docs/java/datastore">
335 * datastore documentation</a>.
337 * @return {@code this} (for chaining)
339 * @throws NullPointerException If {@code propertyName} or {@code operator}
340 * is null.
341 * @throws IllegalArgumentException If {@code value} is not of a
342 * type supported by the datastore. See
343 * {@link DataTypeUtils#isSupportedType(Class)}. Note that unlike
344 * {@link Entity#setProperty(String, Object)}, you cannot provide
345 * a {@link Collection} containing instances of supported types
346 * to this method.
347 * @deprecated Use {@link #setFilter(Filter)}
349 @Deprecated
350 public Query addFilter(String propertyName, FilterOperator operator, Object value) {
351 filterPredicates.add(operator.of(propertyName, value));
352 return this;
356 * Returns a mutable list of the current filter predicates.
358 * @see #addFilter(String, FilterOperator, Object)
359 * @deprecated Use {@link #setFilter(Filter)} and {@link #getFilter()} instead
361 @Deprecated
362 public List<FilterPredicate> getFilterPredicates() {
363 return filterPredicates;
367 * Specify how the query results should be sorted.
369 * The first call to addSort will register the property that will
370 * serve as the primary sort key. A second call to addSort will set
371 * a secondary sort key, etc.
373 * This method will always sort in ascending order. To control the
374 * order of the sort, use {@link #addSort(String,SortDirection)}.
376 * Note that entities with multi-value properties identified by
377 * {@code propertyName} will be sorted by the smallest value in the list.
378 * For more information on sorting properties with multiple values please see
379 * the <a href="http://code.google.com/appengine/docs/java/datastore">
380 * datastore documentation</a>.
382 * @return {@code this} (for chaining)
384 * @throws NullPointerException If any argument is null.
386 public Query addSort(String propertyName) {
387 return addSort(propertyName, SortDirection.ASCENDING);
391 * Specify how the query results should be sorted.
393 * The first call to addSort will register the property that will
394 * serve as the primary sort key. A second call to addSort will set
395 * a secondary sort key, etc.
397 * Note that if {@code direction} is {@link SortDirection#ASCENDING},
398 * entities with multi-value properties identified by
399 * {@code propertyName} will be sorted by the smallest value in the list. If
400 * {@code direction} is {@link SortDirection#DESCENDING}, entities with
401 * multi-value properties identified by {@code propertyName} will be sorted
402 * by the largest value in the list. For more information on sorting
403 * properties with multiple values please see
404 * the <a href="http://code.google.com/appengine/docs/java/datastore">
405 * datastore documentation</a>.
407 * @return {@code this} (for chaining)
409 * @throws NullPointerException If any argument is null.
411 public Query addSort(String propertyName, SortDirection direction) {
412 sortPredicates.add(new SortPredicate(propertyName, direction));
413 return this;
417 * Returns a mutable list of the current sort predicates.
419 public List<SortPredicate> getSortPredicates() {
420 return sortPredicates;
424 * Makes this query fetch and return only keys, not full entities.
426 * @return {@code this} (for chaining)
428 public Query setKeysOnly() {
429 keysOnly = true;
430 return this;
434 * Clears the keys only flag.
436 * @see #setKeysOnly()
437 * @return {@code this} (for chaining)
439 public Query clearKeysOnly() {
440 keysOnly = false;
441 return this;
445 * Adds a projection for this query.
447 * <p>Projections are limited in the following ways:
448 * <ul>
449 * <li>Un-indexed properties cannot be projected and attempting to do so will
450 * result in no entities being returned.
451 * <li>Projection {@link Projection#getName() names} must be unique.
452 * <li>Properties that have an equality filter on them cannot be projected. This includes
453 * the operators {@link FilterOperator#EQUAL} and {@link FilterOperator#IN}.
454 * </ul>
456 * @see #getProjections()
457 * @param projection the projection to add
458 * @return {@code this} (for chaining)
459 * @throws IllegalArgumentException if the query already contains a projection with the same name
461 public Query addProjection(Projection projection) {
462 Preconditions.checkArgument(!projectionMap.containsKey(projection.getName()),
463 "Query already contains projection with name: " + projection.getName());
464 projectionMap.put(projection.getName(), projection);
465 return this;
469 * Returns a mutable collection properties included in the projection for this query.
471 * <p>If empty, the full or keys only entities are returned. Otherwise
472 * partial entities are returned. A non-empty projection is not compatible
473 * with setting keys-only. In this case a {@link IllegalArgumentException}
474 * will be thrown when the query is {@link DatastoreService#prepare(Query) prepared}.
476 * <p>Projection queries are similar to SQL statements of the form:
477 * <pre>SELECT prop1, prop2, ...</pre>
478 * As they return partial entities, which only contain the properties
479 * specified in the projection. However, these entities will only contain
480 * a single value for any multi-valued property and, if a multi-valued
481 * property is specified in the order, an inequality property, or the
482 * projected properties, the entity will be returned multiple times.
483 * Once for each unique combination of values.
485 * <p>Specifying a projection:
486 * <ul>
487 * <li>May change the type of any property returned in a projection.
488 * <li>May change the index requirements for the given query.
489 * <li>Will cause a partial entity to be returned.
490 * <li>Will cause only entities that contain those properties to be returned.
491 * </ul>
492 * However, projection queries are significantly faster than normal queries.
494 * @return a mutable collection properties included in the projection for this query
495 * @see #addProjection(Projection)
497 public Collection<Projection> getProjections() {
498 return projectionMap.values();
502 * Returns true if this query will fetch and return keys only, false if it
503 * will fetch and return full entities.
505 public boolean isKeysOnly() {
506 return keysOnly;
510 * Creates a query sorted in the exact opposite direction as the current one.
512 * This function requires a sort order on {@link Entity#KEY_RESERVED_PROPERTY}
513 * to guarantee that each entity is uniquely identified by the set of properties
514 * used in the sort (which is required to exactly reverse the order of a query).
515 * Advanced users can reverse the sort orders manually if they know the set of
516 * sorted properties meets this requirement without a order on {@link
517 * Entity#KEY_RESERVED_PROPERTY}.
519 * The results of the reverse query will be the same as the results of the forward
520 * query but in reverse direction.
522 * {@link Cursor Cursors} from the original query can be converted for use with
523 * the reverse query through {@link Cursor#reverse()}.
525 * @return A new query with the sort order reversed.
526 * @throws IllegalStateException if the current query is not sorted by
527 * {@link Entity#KEY_RESERVED_PROPERTY}.
529 public Query reverse() {
530 List<SortPredicate> order = new ArrayList<SortPredicate>(sortPredicates.size());
531 boolean hasKeyOrder = false;
532 for (SortPredicate sort : sortPredicates) {
533 if (Entity.KEY_RESERVED_PROPERTY.equals(sort.getPropertyName())) {
534 hasKeyOrder = true;
536 if (!distinct || projectionMap.containsKey(sort.getPropertyName())) {
537 order.add(sort.reverse());
538 } else {
539 order.add(sort);
542 Preconditions.checkState(hasKeyOrder, "A sort on " + Entity.KEY_RESERVED_PROPERTY +
543 " is required to reverse a Query");
545 return new Query(kind, ancestor, order, filter, filterPredicates, keysOnly, appIdNamespace,
546 projectionMap.values(), distinct, fullTextSearch);
550 * Returns the query's full text search string.
552 String getFullTextSearch() {
553 return fullTextSearch;
557 * Sets the query's full text search string.
559 * @return {@code this} (for chaining)
561 Query setFullTextSearch(String fullTextSearch) {
562 this.fullTextSearch = fullTextSearch;
563 return this;
566 @Override
567 public boolean equals(Object o) {
568 if (this == o) {
569 return true;
571 if (o == null || getClass() != o.getClass()) {
572 return false;
575 Query query = (Query) o;
577 if (keysOnly != query.keysOnly) {
578 return false;
581 if (!Objects.equal(ancestor, query.ancestor)) {
582 return false;
584 if (!appIdNamespace.equals(query.appIdNamespace)) {
585 return false;
587 if (!Objects.equal(filter, query.filter)) {
588 return false;
590 if (!filterPredicates.equals(query.filterPredicates)) {
591 return false;
593 if (!kind.equals(query.kind)) {
594 return false;
596 if (!sortPredicates.equals(query.sortPredicates)) {
597 return false;
599 if (!projectionMap.equals(query.projectionMap)) {
600 return false;
602 if (distinct != query.distinct) {
603 return false;
605 if (!Objects.equal(fullTextSearch, query.fullTextSearch)) {
606 return false;
609 return true;
612 @Override
613 public int hashCode() {
614 return Objects.hashCode(kind, sortPredicates, filterPredicates, filter, ancestor, keysOnly,
615 appIdNamespace, projectionMap, distinct, fullTextSearch);
619 * Obtains a hash code of the {@code Query} ignoring any 'value' arguments associated with
620 * any query filters.
622 int hashCodeNoFilterValues() {
623 int filterHashCode = 1;
624 for (FilterPredicate filterPred : filterPredicates) {
625 filterHashCode = 31 * filterHashCode + filterPred.hashCodeNoFilterValues();
627 filterHashCode = 31 * filterHashCode + ((filter == null) ? 0 : filter.hashCodeNoFilterValues());
628 return Objects.hashCode(kind, sortPredicates, filterHashCode, ancestor, keysOnly,
629 appIdNamespace, projectionMap, distinct, fullTextSearch);
633 * Outputs a SQL like string representing the query.
635 @Override
636 public String toString() {
637 StringBuilder result = new StringBuilder("SELECT ");
638 if (distinct) {
639 result.append("DISTINCT ");
641 if (!projectionMap.isEmpty()) {
642 Joiner.on(", ").appendTo(result, projectionMap.values());
643 } else if (keysOnly) {
644 result.append("__key__");
645 } else {
646 result.append('*');
649 if (kind != null) {
650 result.append(" FROM ");
651 result.append(kind);
654 if (ancestor != null || !filterPredicates.isEmpty() || filter != null) {
655 result.append(" WHERE ");
656 final String AND_SEPARATOR = " AND ";
658 if (filter != null) {
659 result.append(filter);
660 } else if (!filterPredicates.isEmpty()) {
661 Joiner.on(AND_SEPARATOR).appendTo(result, filterPredicates);
664 if (ancestor != null) {
665 if (!filterPredicates.isEmpty() || filter != null) {
666 result.append(AND_SEPARATOR);
668 result.append("__ancestor__ is ");
669 result.append(ancestor);
673 if (!sortPredicates.isEmpty()) {
674 result.append(" ORDER BY ");
675 Joiner.on(", ").appendTo(result, sortPredicates);
677 return result.toString();
681 * SortPredicate is a data container that holds a single sort
682 * predicate.
684 public static final class SortPredicate implements Serializable {
685 @SuppressWarnings("hiding")
686 static final long serialVersionUID = -623786024456258081L;
687 private final String propertyName;
688 private final SortDirection direction;
690 public SortPredicate(String propertyName, SortDirection direction) {
691 if (propertyName == null) {
692 throw new NullPointerException("Property name was null");
695 if (direction == null) {
696 throw new NullPointerException("Direction was null");
699 this.propertyName = propertyName;
700 this.direction = direction;
704 * @return A sort predicate with the direction reversed.
706 public SortPredicate reverse() {
707 return new SortPredicate(propertyName,
708 direction == SortDirection.ASCENDING ?
709 SortDirection.DESCENDING : SortDirection.ASCENDING);
713 * Gets the name of the property to sort on.
715 public String getPropertyName() {
716 return propertyName;
720 * Gets the direction of the sort.
722 public SortDirection getDirection() {
723 return direction;
726 @Override
727 public boolean equals(Object o) {
728 if (this == o) {
729 return true;
731 if (o == null || getClass() != o.getClass()) {
732 return false;
735 SortPredicate that = (SortPredicate) o;
737 if (direction != that.direction) {
738 return false;
740 if (!propertyName.equals(that.propertyName)) {
741 return false;
744 return true;
747 @Override
748 public int hashCode() {
749 int result;
750 result = propertyName.hashCode();
751 result = 31 * result + direction.hashCode();
752 return result;
755 @Override
756 public String toString() {
757 return propertyName + (direction == SortDirection.DESCENDING ? " DESC" : "");
762 * The base class for a query filter.
764 * All sub classes should be immutable.
766 public abstract static class Filter implements Serializable {
767 @SuppressWarnings("hiding")
768 static final long serialVersionUID = -845113806195204425L;
770 Filter() {}
773 * Obtains the hash code for the {@code Filter} ignoring any value parameters associated
774 * with the filter.
776 abstract int hashCodeNoFilterValues();
780 * A {@link Filter} that combines several sub filters using a {@link CompositeFilterOperator}.
782 * For example, to construct a filter of the form <code>a = 1 AND (b = 2 OR c = 3)</code> use:
783 * <pre> {@code
784 * new CompositeFilter(CompositeFilterOperator.AND, Arrays.asList(
785 * new FilterPredicate("a", FilterOperator.EQUAL, 1),
786 * new CompositeFilter(CompositeFilterOperator.OR, Arrays.<Filter>asList(
787 * new FilterPredicate("b", FilterOperator.EQUAL, 2),
788 * new FilterPredicate("c", FilterOperator.EQUAL, 3)))));
789 * }</pre>
790 * or
791 * <pre> {@code
792 * CompositeFilterOperator.and(
793 * FilterOperator.EQUAL.of("a", 1),
794 * CompositeFilterOperator.or(
795 * FilterOperator.EQUAL.of("b", 2),
796 * FilterOperator.EQUAL.of("c", 3)));
797 * }</pre>
799 public static final class CompositeFilter extends Filter {
800 @SuppressWarnings("hiding")
801 static final long serialVersionUID = 7930286402872420509L;
803 private final CompositeFilterOperator operator;
804 private final ImmutableList<Filter> subFilters;
806 public CompositeFilter(CompositeFilterOperator operator, Collection<Filter> subFilters) {
807 Preconditions.checkArgument(subFilters.size() >= 2, "At least two sub filters are required.");
808 this.operator = checkNotNull(operator);
809 this.subFilters = ImmutableList.copyOf(subFilters);
813 * @return the operator
815 public CompositeFilterOperator getOperator() {
816 return operator;
820 * @return an immutable list of sub filters
822 public List<Filter> getSubFilters() {
823 return subFilters;
826 @Override
827 public String toString() {
828 StringBuilder builder = new StringBuilder();
829 builder.append('(');
830 Joiner.on(" " + operator + " ").appendTo(builder, subFilters);
831 builder.append(')');
832 return builder.toString();
835 @Override
836 public int hashCode() {
837 return Objects.hashCode(operator, subFilters);
840 @Override
841 int hashCodeNoFilterValues() {
842 int result = 1;
843 result = 31 * result + operator.hashCode();
844 for (Filter filter : subFilters) {
845 result = 31 * result + filter.hashCodeNoFilterValues();
847 return result;
850 @Override
851 public boolean equals(Object obj) {
852 if (this == obj) return true;
853 if (!(obj instanceof CompositeFilter)) return false;
854 CompositeFilter other = (CompositeFilter) obj;
855 if (operator != other.operator) return false;
856 return subFilters.equals(other.subFilters);
861 * A {@link Filter} on a single property.
863 public static final class FilterPredicate extends Filter {
864 @SuppressWarnings("hiding")
865 static final long serialVersionUID = 7681475799401864259L;
866 private final String propertyName;
867 private final FilterOperator operator;
868 private final Object value;
871 * Constructs a filter predicate from the given parameters.
873 * @param propertyName the name of the property on which to filter
874 * @param operator the operator to apply
875 * @param value A single instances of a supported type or if {@code
876 * operator} is {@link FilterOperator#IN} a non-empty {@link Iterable}
877 * object containing instances of supported types.
879 * @throws IllegalArgumentException If the provided filter values are not
880 * supported.
882 * @see DataTypeUtils#isSupportedType(Class)
885 public FilterPredicate(String propertyName, FilterOperator operator, Object value) {
886 if (propertyName == null) {
887 throw new NullPointerException("Property name was null");
888 } else if (operator == null) {
889 throw new NullPointerException("Operator was null");
890 } else if (operator == FilterOperator.IN) {
891 if (!(value instanceof Collection<?>) && value instanceof Iterable<?>) {
892 List<Object> newValue = new ArrayList<Object>();
893 for (Object val : (Iterable<?>) value) {
894 newValue.add(val);
896 value = newValue;
898 DataTypeUtils.checkSupportedValue(propertyName, value, true, true);
899 } else {
900 DataTypeUtils.checkSupportedValue(propertyName, value, false, false);
902 this.propertyName = propertyName;
903 this.operator = operator;
904 this.value = value;
908 * Gets the name of the property to be filtered on.
910 public String getPropertyName() {
911 return propertyName;
915 * Gets the operator describing how to apply the filter.
917 public FilterOperator getOperator() {
918 return operator;
922 * Gets the argument to the filter operator.
924 public Object getValue() {
925 return value;
928 @Override
929 public boolean equals(Object o) {
930 if (this == o) {
931 return true;
933 if (o == null || getClass() != o.getClass()) {
934 return false;
937 FilterPredicate that = (FilterPredicate) o;
939 if (operator != that.operator) {
940 return false;
942 if (!propertyName.equals(that.propertyName)) {
943 return false;
945 if (!Objects.equal(value, that.value)) {
946 return false;
949 return true;
952 @Override
953 public int hashCode() {
954 return Objects.hashCode(propertyName, operator, value);
957 @Override
958 int hashCodeNoFilterValues() {
959 return Objects.hashCode(propertyName, operator);
962 @Override
963 public String toString() {
964 return propertyName + " " + operator.toString() + " " +
965 (value != null ? value.toString() : "NULL");