App Engine Java SDK version 1.9.25
[gae.git] / java / src / main / com / google / appengine / api / datastore / Query.java
bloba77dabda29a60c1a5fcb1318a8a05167f6033ff5
1 package com.google.appengine.api.datastore;
3 import static com.google.common.base.Preconditions.checkArgument;
4 import static com.google.common.base.Preconditions.checkNotNull;
6 import com.google.common.base.Joiner;
7 import com.google.common.base.Preconditions;
8 import com.google.common.collect.ImmutableList;
9 import com.google.common.collect.Iterables;
10 import com.google.common.collect.Lists;
11 import com.google.common.collect.Maps;
13 import java.io.IOException;
14 import java.io.ObjectInputStream;
15 import java.io.Serializable;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Objects;
23 /**
24 * {@link Query} encapsulates a request for zero or more {@link Entity} objects
25 * out of the datastore. It supports querying on zero or more properties,
26 * querying by ancestor, and sorting. {@link Entity} objects which match the
27 * query can be retrieved in a single list, or with an unbounded iterator.
29 public final class Query implements Serializable {
31 @Deprecated
32 public static final String KIND_METADATA_KIND = Entities.KIND_METADATA_KIND;
34 @Deprecated
35 public static final String PROPERTY_METADATA_KIND = Entities.PROPERTY_METADATA_KIND;
37 @Deprecated
38 public static final String NAMESPACE_METADATA_KIND = Entities.NAMESPACE_METADATA_KIND;
40 static final long serialVersionUID = 7090652715949085374L;
42 /**
43 * SortDirection controls the order of a sort.
45 public enum SortDirection {
46 ASCENDING,
47 DESCENDING
50 /**
51 * Operators supported by {@link FilterPredicate}.
53 public enum FilterOperator {
54 LESS_THAN("<"),
55 LESS_THAN_OR_EQUAL("<="),
56 GREATER_THAN(">"),
57 GREATER_THAN_OR_EQUAL(">="),
58 EQUAL("="),
59 NOT_EQUAL("!="),
60 IN("IN");
62 private final String shortName;
63 private FilterOperator(String shortName) {
64 this.shortName = shortName;
67 @Override
68 public String toString() {
69 return shortName;
72 public FilterPredicate of(String propertyName, Object value) {
73 return new FilterPredicate(propertyName, this, value);
77 /**
78 * Operators supported by {@link CompositeFilter}.
80 public enum CompositeFilterOperator {
81 AND,
82 OR;
84 public static CompositeFilter and(Filter... subFilters) {
85 return and(Arrays.asList(subFilters));
88 public static CompositeFilter and(Collection<Filter> subFilters) {
89 return new CompositeFilter(AND, subFilters);
92 public static CompositeFilter or(Filter... subFilters) {
93 return or(Arrays.asList(subFilters));
96 public static CompositeFilter or(Collection<Filter> subFilters) {
97 return new CompositeFilter(OR, subFilters);
100 public CompositeFilter of(Filter... subFilters) {
101 return new CompositeFilter(this, Arrays.asList(subFilters));
104 public CompositeFilter of(Collection<Filter> subFilters) {
105 return new CompositeFilter(this, subFilters);
109 private final String kind;
110 private final List<SortPredicate> sortPredicates = Lists.newArrayList();
111 private final List<FilterPredicate> filterPredicates = Lists.newArrayList();
112 private Filter filter;
113 private Key ancestor;
114 private boolean keysOnly;
115 private final Map<String, Projection> projectionMap = Maps.newLinkedHashMap();
116 private boolean distinct;
117 private AppIdNamespace appIdNamespace;
120 * Create a new kindless {@link Query} that finds {@link Entity} objects.
121 * Note that kindless queries are not yet supported in the Java dev
122 * appserver.
124 * Currently the only operations supported on a kindless query are filter by
125 * __key__, ancestor, and order by __key__ ascending.
127 public Query() {
128 this(null, null);
132 * Create a new {@link Query} that finds {@link Entity} objects with
133 * the specified {@code kind}. Note that kindless queries are not yet
134 * supported in the Java dev appserver.
136 * @param kind the kind or null to create a kindless query
138 public Query(String kind) {
139 this(kind, null);
143 * Copy constructor that performs a deep copy of the provided Query.
145 * @param query The Query to copy.
147 Query(Query query) {
148 this(query.kind, query.ancestor, query.sortPredicates, query.filter, query.filterPredicates,
149 query.keysOnly, query.appIdNamespace, query.projectionMap.values(), query.distinct);
152 Query(String kind, Key ancestor, Filter filter, boolean keysOnly, AppIdNamespace appIdNamespace,
153 boolean distinct) {
154 this.kind = kind;
155 this.keysOnly = keysOnly;
156 this.appIdNamespace = appIdNamespace;
157 this.distinct = distinct;
158 this.filter = filter;
159 if (ancestor != null) {
160 setAncestor(ancestor);
164 Query(String kind, Key ancestor, Collection<SortPredicate> sortPreds,
165 Filter filter,
166 Collection<FilterPredicate> filterPreds, boolean keysOnly, AppIdNamespace appIdNamespace,
167 Collection<Projection> projections, boolean distinct) {
168 this(kind, ancestor, filter, keysOnly, appIdNamespace, distinct);
169 this.sortPredicates.addAll(sortPreds);
170 this.filterPredicates.addAll(filterPreds);
171 for (Projection projection : projections) {
172 addProjection(projection);
176 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
177 in.defaultReadObject();
178 if (appIdNamespace == null) {
179 if (ancestor != null) {
180 appIdNamespace = ancestor.getAppIdNamespace();
181 } else {
182 appIdNamespace = new AppIdNamespace(DatastoreApiHelper.getCurrentAppId(), "");
188 * Create a new {@link Query} that finds {@link Entity} objects with
189 * the specified {@code Key} as an ancestor.
191 * @param ancestor the ancestor key or null
192 * @throws IllegalArgumentException If ancestor is not complete.
194 public Query(Key ancestor) {
195 this(null, ancestor);
199 * Create a new {@link Query} that finds {@link Entity} objects with
200 * the specified {@code kind} and the specified {@code ancestor}. Note that
201 * kindless queries are not yet supported in the Java dev appserver.
203 * @param kind the kind or null to create a kindless query
204 * @param ancestor the ancestor key or null
205 * @throws IllegalArgumentException If the ancestor is not complete.
207 public Query(String kind, Key ancestor) {
208 this(kind, ancestor, null, false, DatastoreApiHelper.getCurrentAppIdNamespace(), false);
212 * Only {@link Entity} objects whose kind matches this value will be
213 * returned.
215 public String getKind() {
216 return kind;
220 * Returns the AppIdNamespace that is being queried.
221 * <p>The AppIdNamespace is set at construction time of this
222 * object using the {@link NamespaceManager} to retrieve the
223 * current namespace.
225 AppIdNamespace getAppIdNamespace() {
226 return appIdNamespace;
230 * Returns the appId for this {@link Query}.
232 public String getAppId() {
233 return appIdNamespace.getAppId();
237 * Returns the namespace for this {@link Query}.
239 public String getNamespace() {
240 return appIdNamespace.getNamespace();
244 * Gets the current ancestor for this query, or null if there is no
245 * ancestor specified.
247 public Key getAncestor() {
248 return ancestor;
252 * Sets an ancestor for this query.
254 * This restricts the query to only return result entities that are
255 * descended from a given entity. In other words, all of the results
256 * will have the ancestor as their parent, or parent's parent, or
257 * etc.
259 * If null is specified, unsets any previously-set ancestor. Passing
260 * {@code null} as a parameter does not query for entities without
261 * ancestors (this type of query is not currently supported).
263 * @return {@code this} (for chaining)
265 * @throws IllegalArgumentException If the ancestor key is incomplete, or if
266 * you try to unset an ancestor and have not set a kind, or if you try to
267 * unset an ancestor and have not previously set an ancestor.
269 public Query setAncestor(Key ancestor) {
270 if (ancestor != null && !ancestor.isComplete()) {
271 throw new IllegalArgumentException(ancestor + " is incomplete.");
272 } else if (ancestor == null) {
273 if (this.ancestor == null) {
274 throw new IllegalArgumentException(
275 "Cannot clear ancestor unless ancestor has already been set");
278 if (ancestor != null) {
279 if (!ancestor.getAppIdNamespace().equals(appIdNamespace)) {
280 throw new IllegalArgumentException(
281 "Namespace of ancestor key and query must match.");
284 this.ancestor = ancestor;
285 return this;
289 * @param distinct if this query should be distinct. This may only be used
290 * when the query has a projection.
291 * @return {@code this} (for chaining)
293 public Query setDistinct(boolean distinct) {
294 this.distinct = distinct;
295 return this;
299 * @return if this query is distinct
300 * @see #setDistinct(boolean)
302 public boolean getDistinct() {
303 return distinct;
307 * @param filter the filter to use for this query, or {@code null}
308 * @return {@code this} (for chaining)
309 * @see CompositeFilter
310 * @see FilterPredicate
312 public Query setFilter(Filter filter) {
313 this.filter = filter;
314 return this;
318 * @return the filter for this query or {@code null}
319 * @see #setFilter(Filter)
321 public Filter getFilter() {
322 return filter;
326 * Add a {@link FilterPredicate} on the specified property.
328 * <p>All {@link FilterPredicate}s added using this message are combined using
329 * {@link CompositeFilterOperator#AND}.
331 * <p>Cannot be used in conjunction with {@link #setFilter(Filter)} which sets
332 * a single {@link Filter} instead of many {@link FilterPredicate}s.
334 * @param propertyName The name of the property to which the filter applies.
335 * @param operator The filter operator.
336 * @param value An instance of a supported datastore type. Note that
337 * entities with multi-value properties identified by {@code propertyName}
338 * will match this filter if the multi-value property has at least one
339 * value that matches the condition expressed by {@code operator} and
340 * {@code value}. For more information on multi-value property filtering
341 * please see the
342 * <a href="http://cloud.google.com/appengine/docs/java/datastore/">
343 * datastore documentation</a>.
345 * @return {@code this} (for chaining)
347 * @throws NullPointerException If {@code propertyName} or {@code operator}
348 * is null.
349 * @throws IllegalArgumentException If {@code value} is not of a
350 * type supported by the datastore. See
351 * {@link DataTypeUtils#isSupportedType(Class)}. Note that unlike
352 * {@link Entity#setProperty(String, Object)}, you cannot provide
353 * a {@link Collection} containing instances of supported types
354 * to this method.
355 * @deprecated Use {@link #setFilter(Filter)}
357 @Deprecated
358 public Query addFilter(String propertyName, FilterOperator operator, Object value) {
359 filterPredicates.add(operator.of(propertyName, value));
360 return this;
364 * Returns a mutable list of the current filter predicates.
366 * @see #addFilter(String, FilterOperator, Object)
367 * @deprecated Use {@link #setFilter(Filter)} and {@link #getFilter()} instead
369 @Deprecated
370 public List<FilterPredicate> getFilterPredicates() {
371 return filterPredicates;
375 * Specify how the query results should be sorted.
377 * The first call to addSort will register the property that will
378 * serve as the primary sort key. A second call to addSort will set
379 * a secondary sort key, etc.
381 * This method will always sort in ascending order. To control the
382 * order of the sort, use {@link #addSort(String,SortDirection)}.
384 * Note that entities with multi-value properties identified by
385 * {@code propertyName} will be sorted by the smallest value in the list.
386 * For more information on sorting properties with multiple values please see
387 * the <a href="http://cloud.google.com/appengine/docs/java/datastore/">
388 * datastore documentation</a>.
390 * @return {@code this} (for chaining)
392 * @throws NullPointerException If any argument is null.
394 public Query addSort(String propertyName) {
395 return addSort(propertyName, SortDirection.ASCENDING);
399 * Specify how the query results should be sorted.
401 * The first call to addSort will register the property that will
402 * serve as the primary sort key. A second call to addSort will set
403 * a secondary sort key, etc.
405 * Note that if {@code direction} is {@link SortDirection#ASCENDING},
406 * entities with multi-value properties identified by
407 * {@code propertyName} will be sorted by the smallest value in the list. If
408 * {@code direction} is {@link SortDirection#DESCENDING}, entities with
409 * multi-value properties identified by {@code propertyName} will be sorted
410 * by the largest value in the list. For more information on sorting
411 * properties with multiple values please see
412 * the <a href="http://cloud.google.com/appengine/docs/java/datastore/">
413 * datastore documentation</a>.
415 * @return {@code this} (for chaining)
417 * @throws NullPointerException If any argument is null.
419 public Query addSort(String propertyName, SortDirection direction) {
420 sortPredicates.add(new SortPredicate(propertyName, direction));
421 return this;
425 * Returns a mutable list of the current sort predicates.
427 public List<SortPredicate> getSortPredicates() {
428 return sortPredicates;
432 * Makes this query fetch and return only keys, not full entities.
434 * @return {@code this} (for chaining)
436 public Query setKeysOnly() {
437 keysOnly = true;
438 return this;
442 * Clears the keys only flag.
444 * @see #setKeysOnly()
445 * @return {@code this} (for chaining)
447 public Query clearKeysOnly() {
448 keysOnly = false;
449 return this;
453 * Adds a projection for this query.
455 * <p>Projections are limited in the following ways:
456 * <ul>
457 * <li>Un-indexed properties cannot be projected and attempting to do so will
458 * result in no entities being returned.
459 * <li>Projection {@link Projection#getName() names} must be unique.
460 * <li>Properties that have an equality filter on them cannot be projected. This includes
461 * the operators {@link FilterOperator#EQUAL} and {@link FilterOperator#IN}.
462 * </ul>
464 * @see #getProjections()
465 * @param projection the projection to add
466 * @return {@code this} (for chaining)
467 * @throws IllegalArgumentException if the query already contains a projection with the same name
469 public Query addProjection(Projection projection) {
470 Preconditions.checkArgument(!projectionMap.containsKey(projection.getName()),
471 "Query already contains projection with name: " + projection.getName());
472 projectionMap.put(projection.getName(), projection);
473 return this;
477 * Returns a mutable collection properties included in the projection for this query.
479 * <p>If empty, the full or keys only entities are returned. Otherwise
480 * partial entities are returned. A non-empty projection is not compatible
481 * with setting keys-only. In this case a {@link IllegalArgumentException}
482 * will be thrown when the query is {@link DatastoreService#prepare(Query) prepared}.
484 * <p>Projection queries are similar to SQL statements of the form:
485 * <pre>SELECT prop1, prop2, ...</pre>
486 * As they return partial entities, which only contain the properties
487 * specified in the projection. However, these entities will only contain
488 * a single value for any multi-valued property and, if a multi-valued
489 * property is specified in the order, an inequality property, or the
490 * projected properties, the entity will be returned multiple times.
491 * Once for each unique combination of values.
493 * <p>Specifying a projection:
494 * <ul>
495 * <li>May change the type of any property returned in a projection.
496 * <li>May change the index requirements for the given query.
497 * <li>Will cause a partial entity to be returned.
498 * <li>Will cause only entities that contain those properties to be returned.
499 * </ul>
500 * However, projection queries are significantly faster than normal queries.
502 * @return a mutable collection properties included in the projection for this query
503 * @see #addProjection(Projection)
505 public Collection<Projection> getProjections() {
506 return projectionMap.values();
510 * Returns true if this query will fetch and return keys only, false if it
511 * will fetch and return full entities.
513 public boolean isKeysOnly() {
514 return keysOnly;
518 * Creates a query sorted in the exact opposite direction as the current one.
520 * This function requires a sort order on {@link Entity#KEY_RESERVED_PROPERTY}
521 * to guarantee that each entity is uniquely identified by the set of properties
522 * used in the sort (which is required to exactly reverse the order of a query).
523 * Advanced users can reverse the sort orders manually if they know the set of
524 * sorted properties meets this requirement without a order on {@link
525 * Entity#KEY_RESERVED_PROPERTY}.
527 * The results of the reverse query will be the same as the results of the forward
528 * query but in reverse direction.
530 * {@link Cursor Cursors} from the original query may also be used in the
531 * reverse query.
533 * @return A new query with the sort order reversed.
534 * @throws IllegalStateException if the current query is not sorted by
535 * {@link Entity#KEY_RESERVED_PROPERTY}.
537 public Query reverse() {
538 List<SortPredicate> order = new ArrayList<SortPredicate>(sortPredicates.size());
539 boolean hasKeyOrder = false;
540 for (SortPredicate sort : sortPredicates) {
541 if (Entity.KEY_RESERVED_PROPERTY.equals(sort.getPropertyName())) {
542 hasKeyOrder = true;
544 if (!distinct || projectionMap.containsKey(sort.getPropertyName())) {
545 order.add(sort.reverse());
546 } else {
547 order.add(sort);
550 Preconditions.checkState(hasKeyOrder, "A sort on " + Entity.KEY_RESERVED_PROPERTY +
551 " is required to reverse a Query");
553 return new Query(kind, ancestor, order, filter, filterPredicates, keysOnly, appIdNamespace,
554 projectionMap.values(), distinct);
557 @Override
558 public boolean equals(Object o) {
559 if (this == o) {
560 return true;
562 if (o == null || getClass() != o.getClass()) {
563 return false;
566 Query query = (Query) o;
568 if (keysOnly != query.keysOnly) {
569 return false;
572 if (!Objects.equals(ancestor, query.ancestor)) {
573 return false;
575 if (!appIdNamespace.equals(query.appIdNamespace)) {
576 return false;
578 if (!Objects.equals(filter, query.filter)) {
579 return false;
581 if (!filterPredicates.equals(query.filterPredicates)) {
582 return false;
584 if (!Objects.equals(kind, query.kind)) {
585 return false;
587 if (!sortPredicates.equals(query.sortPredicates)) {
588 return false;
590 if (!projectionMap.equals(query.projectionMap)) {
591 return false;
593 if (distinct != query.distinct) {
594 return false;
597 return true;
600 @Override
601 public int hashCode() {
602 return Objects.hash(kind, sortPredicates, filterPredicates, filter, ancestor, keysOnly,
603 appIdNamespace, projectionMap, distinct);
607 * Obtains a hash code of the {@code Query} ignoring any 'value' arguments associated with
608 * any query filters.
610 int hashCodeNoFilterValues() {
611 int filterHashCode = 1;
612 for (FilterPredicate filterPred : filterPredicates) {
613 filterHashCode = 31 * filterHashCode + filterPred.hashCodeNoFilterValues();
615 filterHashCode = 31 * filterHashCode + ((filter == null) ? 0 : filter.hashCodeNoFilterValues());
616 return Objects.hash(kind, sortPredicates, filterHashCode, ancestor, keysOnly,
617 appIdNamespace, projectionMap, distinct);
621 * Outputs a SQL like string representing the query.
623 @Override
624 public String toString() {
625 StringBuilder result = new StringBuilder("SELECT ");
626 if (distinct) {
627 result.append("DISTINCT ");
629 if (!projectionMap.isEmpty()) {
630 Joiner.on(", ").appendTo(result, projectionMap.values());
631 } else if (keysOnly) {
632 result.append("__key__");
633 } else {
634 result.append('*');
637 if (kind != null) {
638 result.append(" FROM ");
639 result.append(kind);
642 if (ancestor != null || !filterPredicates.isEmpty() || filter != null) {
643 result.append(" WHERE ");
644 final String AND_SEPARATOR = " AND ";
646 if (filter != null) {
647 result.append(filter);
648 } else if (!filterPredicates.isEmpty()) {
649 Joiner.on(AND_SEPARATOR).appendTo(result, filterPredicates);
652 if (ancestor != null) {
653 if (!filterPredicates.isEmpty() || filter != null) {
654 result.append(AND_SEPARATOR);
656 result.append("__ancestor__ is ");
657 result.append(ancestor);
661 if (!sortPredicates.isEmpty()) {
662 result.append(" ORDER BY ");
663 Joiner.on(", ").appendTo(result, sortPredicates);
665 return result.toString();
669 * SortPredicate is a data container that holds a single sort
670 * predicate.
672 public static final class SortPredicate implements Serializable {
673 @SuppressWarnings("hiding")
674 static final long serialVersionUID = -623786024456258081L;
675 private final String propertyName;
676 private final SortDirection direction;
678 public SortPredicate(String propertyName, SortDirection direction) {
679 if (propertyName == null) {
680 throw new NullPointerException("Property name was null");
683 if (direction == null) {
684 throw new NullPointerException("Direction was null");
687 this.propertyName = propertyName;
688 this.direction = direction;
692 * @return A sort predicate with the direction reversed.
694 public SortPredicate reverse() {
695 return new SortPredicate(propertyName,
696 direction == SortDirection.ASCENDING ?
697 SortDirection.DESCENDING : SortDirection.ASCENDING);
701 * Gets the name of the property to sort on.
703 public String getPropertyName() {
704 return propertyName;
708 * Gets the direction of the sort.
710 public SortDirection getDirection() {
711 return direction;
714 @Override
715 public boolean equals(Object o) {
716 if (this == o) {
717 return true;
719 if (o == null || getClass() != o.getClass()) {
720 return false;
723 SortPredicate that = (SortPredicate) o;
725 if (direction != that.direction) {
726 return false;
728 if (!propertyName.equals(that.propertyName)) {
729 return false;
732 return true;
735 @Override
736 public int hashCode() {
737 int result;
738 result = propertyName.hashCode();
739 result = 31 * result + direction.hashCode();
740 return result;
743 @Override
744 public String toString() {
745 return propertyName + (direction == SortDirection.DESCENDING ? " DESC" : "");
750 * The base class for a query filter.
752 * All sub classes should be immutable.
754 public abstract static class Filter implements Serializable {
755 @SuppressWarnings("hiding")
756 static final long serialVersionUID = -845113806195204425L;
758 Filter() {}
761 * Obtains the hash code for the {@code Filter} ignoring any value parameters associated
762 * with the filter.
764 abstract int hashCodeNoFilterValues();
768 * A {@link Filter} that combines several sub filters using a {@link CompositeFilterOperator}.
770 * For example, to construct a filter of the form <code>a = 1 AND (b = 2 OR c = 3)</code> use:
771 * <pre> {@code
772 * new CompositeFilter(CompositeFilterOperator.AND, Arrays.asList(
773 * new FilterPredicate("a", FilterOperator.EQUAL, 1),
774 * new CompositeFilter(CompositeFilterOperator.OR, Arrays.<Filter>asList(
775 * new FilterPredicate("b", FilterOperator.EQUAL, 2),
776 * new FilterPredicate("c", FilterOperator.EQUAL, 3)))));
777 * }</pre>
778 * or
779 * <pre> {@code
780 * CompositeFilterOperator.and(
781 * FilterOperator.EQUAL.of("a", 1),
782 * CompositeFilterOperator.or(
783 * FilterOperator.EQUAL.of("b", 2),
784 * FilterOperator.EQUAL.of("c", 3)));
785 * }</pre>
787 public static final class CompositeFilter extends Filter {
788 @SuppressWarnings("hiding")
789 static final long serialVersionUID = 7930286402872420509L;
791 private final CompositeFilterOperator operator;
792 private final ImmutableList<Filter> subFilters;
794 public CompositeFilter(CompositeFilterOperator operator, Collection<Filter> subFilters) {
795 Preconditions.checkArgument(subFilters.size() >= 2, "At least two sub filters are required.");
796 this.operator = checkNotNull(operator);
797 this.subFilters = ImmutableList.copyOf(subFilters);
801 * @return the operator
803 public CompositeFilterOperator getOperator() {
804 return operator;
808 * @return an immutable list of sub filters
810 public List<Filter> getSubFilters() {
811 return subFilters;
814 @Override
815 public String toString() {
816 StringBuilder builder = new StringBuilder();
817 builder.append('(');
818 Joiner.on(" " + operator + " ").appendTo(builder, subFilters);
819 builder.append(')');
820 return builder.toString();
823 @Override
824 public int hashCode() {
825 return Objects.hash(operator, subFilters);
828 @Override
829 int hashCodeNoFilterValues() {
830 int result = 1;
831 result = 31 * result + operator.hashCode();
832 for (Filter filter : subFilters) {
833 result = 31 * result + filter.hashCodeNoFilterValues();
835 return result;
838 @Override
839 public boolean equals(Object obj) {
840 if (this == obj) return true;
841 if (!(obj instanceof CompositeFilter)) return false;
842 CompositeFilter other = (CompositeFilter) obj;
843 if (operator != other.operator) return false;
844 return subFilters.equals(other.subFilters);
849 * A {@link Filter} on a single property.
851 public static final class FilterPredicate extends Filter {
852 @SuppressWarnings("hiding")
853 static final long serialVersionUID = 7681475799401864259L;
854 private final String propertyName;
855 private final FilterOperator operator;
856 private final Object value;
859 * Constructs a filter predicate from the given parameters.
861 * @param propertyName the name of the property on which to filter
862 * @param operator the operator to apply
863 * @param value A single instances of a supported type or if {@code
864 * operator} is {@link FilterOperator#IN} a non-empty {@link Iterable}
865 * object containing instances of supported types.
867 * @throws IllegalArgumentException If the provided filter values are not
868 * supported.
870 * @see DataTypeUtils#isSupportedType(Class)
873 public FilterPredicate(String propertyName, FilterOperator operator, Object value) {
874 if (propertyName == null) {
875 throw new NullPointerException("Property name was null");
876 } else if (operator == null) {
877 throw new NullPointerException("Operator was null");
878 } else if (operator == FilterOperator.IN) {
879 if (!(value instanceof Collection<?>) && value instanceof Iterable<?>) {
880 List<Object> newValue = new ArrayList<Object>();
881 Iterables.addAll(newValue, (Iterable<?>) value);
882 value = newValue;
884 DataTypeUtils.checkSupportedValue(propertyName, value, true, true, false);
885 } else {
886 DataTypeUtils.checkSupportedValue(propertyName, value, false, false, false);
888 this.propertyName = propertyName;
889 this.operator = operator;
890 this.value = value;
894 * Gets the name of the property to be filtered on.
896 public String getPropertyName() {
897 return propertyName;
901 * Gets the operator describing how to apply the filter.
903 public FilterOperator getOperator() {
904 return operator;
908 * Gets the argument to the filter operator.
910 public Object getValue() {
911 return value;
914 @Override
915 public boolean equals(Object o) {
916 if (this == o) {
917 return true;
919 if (o == null || getClass() != o.getClass()) {
920 return false;
923 FilterPredicate that = (FilterPredicate) o;
925 if (operator != that.operator) {
926 return false;
928 if (!propertyName.equals(that.propertyName)) {
929 return false;
931 if (!Objects.equals(value, that.value)) {
932 return false;
935 return true;
938 @Override
939 public int hashCode() {
940 return Objects.hash(propertyName, operator, value);
943 @Override
944 int hashCodeNoFilterValues() {
945 return Objects.hash(propertyName, operator);
948 @Override
949 public String toString() {
950 return propertyName + " " + operator + " " + (value != null ? value : "NULL");
955 * A {@link Filter} representing a geo-region containment predicate.
957 public static final class StContainsFilter extends Filter {
958 private static final long serialVersionUID = 1L;
959 private final String propertyName;
960 private final GeoRegion region;
963 * Constructs a geo-region filter from the given arguments.
965 * @param propertyName the name of the property on which to test containment
966 * @param region a geo-region object against which to test the property value
968 public StContainsFilter(String propertyName, GeoRegion region) {
969 this.propertyName = checkNotNull(propertyName);
970 this.region = checkNotNull(region);
973 public String getPropertyName() {
974 return propertyName;
977 public GeoRegion getRegion() {
978 return region;
981 @Override
982 public boolean equals(Object o) {
983 if (this == o) {
984 return true;
986 if (o == null || getClass() != o.getClass()) {
987 return false;
990 StContainsFilter that = (StContainsFilter) o;
992 if (!propertyName.equals(that.propertyName)) {
993 return false;
995 if (!region.equals(that.region)) {
996 return false;
998 return true;
1001 @Override
1002 public int hashCode() {
1003 return Objects.hash(propertyName, region);
1006 @Override
1007 int hashCodeNoFilterValues() {
1008 return propertyName.hashCode();
1011 @Override
1012 public String toString() {
1013 return String.format("StContainsFilter [%s: %s]", propertyName, region);
1018 * A geographic region intended for use in a
1019 * {@link StContainsFilter}. Note that this is the only purpose for
1020 * which it should be used: in particular, it is not suitable as a
1021 * Property value to be stored in Datastore.
1023 public abstract static class GeoRegion implements Serializable {
1025 GeoRegion() {}
1028 * Determines whether the given {@link GeoPt} value lies within this
1029 * geographic region. If the point lies on the border of the region
1030 * it is considered to be contained.
1032 public abstract boolean contains(GeoPt point);
1035 * A geographical region representing all points within a fixed
1036 * distance from a central point, i.e., a circle. Intended for use in
1037 * a geo-containment predicate filter.
1039 public static final class Circle extends GeoRegion {
1040 private static final long serialVersionUID = 1L;
1042 private final GeoPt center;
1043 private final double radius;
1046 * Creates a new {@code Circle} object from the given arguments.
1048 * @param center a {@code GeoPt} representing the center of the circle
1049 * @param radius the radius of the circle, expressed in meters
1051 public Circle(GeoPt center, double radius) {
1052 this.center = checkNotNull(center);
1053 checkArgument(radius >= 0);
1054 this.radius = radius;
1057 @Override
1058 public boolean contains(GeoPt point) {
1059 return GeoPt.distance(center, point) <= radius;
1062 public GeoPt getCenter() {
1063 return center;
1066 public double getRadius() {
1067 return radius;
1070 @Override
1071 public boolean equals(Object o) {
1072 if (this == o) {
1073 return true;
1076 if (o == null || getClass() != o.getClass()) {
1077 return false;
1080 Circle other = (Circle) o;
1082 if (!center.equals(other.center)) {
1083 return false;
1085 if (Double.compare(radius, other.radius) != 0) {
1086 return false;
1089 return true;
1092 @Override
1093 public int hashCode() {
1094 return Objects.hash(center, radius);
1097 @Override
1098 public String toString() {
1099 return String.format("Circle [(%s),%f]", center, radius);
1104 * A simple geographical region bounded by two latitude lines, and two
1105 * longitude lines, i.e., a "rectangle". It's not really a rectangle,
1106 * of course, because longitude lines are not really parallel.
1107 * <p>
1108 * Intended for use in a geo-containment predicate filter.
1110 public static final class Rectangle extends GeoRegion {
1111 private static final long serialVersionUID = 1L;
1112 private final GeoPt southwest;
1113 private final GeoPt northeast;
1115 public Rectangle(GeoPt southwest, GeoPt northeast) {
1116 checkNotNull(southwest);
1117 checkNotNull(northeast);
1118 checkArgument(southwest.getLatitude() <= northeast.getLatitude());
1119 this.southwest = southwest;
1120 this.northeast = northeast;
1123 public GeoPt getSouthwest() {
1124 return southwest;
1127 public GeoPt getNortheast() {
1128 return northeast;
1131 @Override
1132 public boolean contains(GeoPt point) {
1133 if (point.getLatitude() > northeast.getLatitude()
1134 || point.getLatitude() < southwest.getLatitude()) {
1135 return false;
1138 if (southwest.getLongitude() > northeast.getLongitude()) {
1139 return point.getLongitude() <= northeast.getLongitude()
1140 || point.getLongitude() >= southwest.getLongitude();
1142 return southwest.getLongitude() <= point.getLongitude()
1143 && point.getLongitude() <= northeast.getLongitude();
1146 @Override
1147 public boolean equals(Object o) {
1148 if (this == o) {
1149 return true;
1152 if (o == null || getClass() != o.getClass()) {
1153 return false;
1156 Rectangle other = (Rectangle) o;
1158 if (!southwest.equals(other.southwest)) {
1159 return false;
1161 if (!northeast.equals(other.northeast)) {
1162 return false;
1165 return true;
1168 @Override
1169 public int hashCode() {
1170 return Objects.hash(southwest, northeast);
1173 @Override
1174 public String toString() {
1175 return String.format("Rectangle [(%s),(%s)]", southwest, northeast);