Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / datastore / Query.java
blob87ff16be00ec58058620ac97bab424c1ff2e7236
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.appengine.api.NamespaceManager;
7 import com.google.common.base.Joiner;
8 import com.google.common.base.Preconditions;
9 import com.google.common.collect.ImmutableList;
10 import com.google.common.collect.Iterables;
11 import com.google.common.collect.Lists;
12 import com.google.common.collect.Maps;
14 import java.io.IOException;
15 import java.io.ObjectInputStream;
16 import java.io.Serializable;
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Objects;
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.
30 public final class Query implements Serializable {
32 @Deprecated
33 public static final String KIND_METADATA_KIND = Entities.KIND_METADATA_KIND;
35 @Deprecated
36 public static final String PROPERTY_METADATA_KIND = Entities.PROPERTY_METADATA_KIND;
38 @Deprecated
39 public static final String NAMESPACE_METADATA_KIND = Entities.NAMESPACE_METADATA_KIND;
41 static final long serialVersionUID = 7090652715949085374L;
43 /**
44 * SortDirection controls the order of a sort.
46 public enum SortDirection {
47 ASCENDING,
48 DESCENDING
51 /**
52 * Operators supported by {@link FilterPredicate}.
54 public enum FilterOperator {
55 LESS_THAN("<"),
56 LESS_THAN_OR_EQUAL("<="),
57 GREATER_THAN(">"),
58 GREATER_THAN_OR_EQUAL(">="),
59 EQUAL("="),
60 NOT_EQUAL("!="),
61 IN("IN");
63 private final String shortName;
64 private FilterOperator(String shortName) {
65 this.shortName = shortName;
68 @Override
69 public String toString() {
70 return shortName;
73 public FilterPredicate of(String propertyName, Object value) {
74 return new FilterPredicate(propertyName, this, value);
78 /**
79 * Operators supported by {@link CompositeFilter}.
81 public enum CompositeFilterOperator {
82 AND,
83 OR;
85 public static CompositeFilter and(Filter... subFilters) {
86 return and(Arrays.asList(subFilters));
89 public static CompositeFilter and(Collection<Filter> subFilters) {
90 return new CompositeFilter(AND, subFilters);
93 public static CompositeFilter or(Filter... subFilters) {
94 return or(Arrays.asList(subFilters));
97 public static CompositeFilter or(Collection<Filter> subFilters) {
98 return new CompositeFilter(OR, subFilters);
101 public CompositeFilter of(Filter... subFilters) {
102 return new CompositeFilter(this, Arrays.asList(subFilters));
105 public CompositeFilter of(Collection<Filter> subFilters) {
106 return new CompositeFilter(this, subFilters);
110 private final String kind;
111 private final List<SortPredicate> sortPredicates = Lists.newArrayList();
112 private final List<FilterPredicate> filterPredicates = Lists.newArrayList();
113 private Filter filter;
114 private Key ancestor;
115 private boolean keysOnly;
116 private final Map<String, Projection> projectionMap = Maps.newLinkedHashMap();
117 private boolean distinct;
118 private AppIdNamespace appIdNamespace;
121 * Create a new kindless {@link Query} that finds {@link Entity} objects.
122 * Note that kindless queries are not yet supported in the Java dev
123 * appserver.
125 * Currently the only operations supported on a kindless query are filter by
126 * __key__, ancestor, and order by __key__ ascending.
128 public Query() {
129 this(null, null);
133 * Create a new {@link Query} that finds {@link Entity} objects with
134 * the specified {@code kind}. Note that kindless queries are not yet
135 * supported in the Java dev appserver.
137 * @param kind the kind or null to create a kindless query
139 public Query(String kind) {
140 this(kind, null);
144 * Copy constructor that performs a deep copy of the provided Query.
146 * @param query The Query to copy.
148 Query(Query query) {
149 this(query.kind, query.ancestor, query.sortPredicates, query.filter, query.filterPredicates,
150 query.keysOnly, query.appIdNamespace, query.projectionMap.values(), query.distinct);
153 Query(String kind, Key ancestor, Filter filter, boolean keysOnly, AppIdNamespace appIdNamespace,
154 boolean distinct) {
155 this.kind = kind;
156 this.keysOnly = keysOnly;
157 this.appIdNamespace = appIdNamespace;
158 this.distinct = distinct;
159 this.filter = filter;
160 if (ancestor != null) {
161 setAncestor(ancestor);
165 Query(String kind, Key ancestor, Collection<SortPredicate> sortPreds,
166 Filter filter,
167 Collection<FilterPredicate> filterPreds, boolean keysOnly, AppIdNamespace appIdNamespace,
168 Collection<Projection> projections, boolean distinct) {
169 this(kind, ancestor, filter, keysOnly, appIdNamespace, distinct);
170 this.sortPredicates.addAll(sortPreds);
171 this.filterPredicates.addAll(filterPreds);
172 for (Projection projection : projections) {
173 addProjection(projection);
177 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
178 in.defaultReadObject();
179 if (appIdNamespace == null) {
180 if (ancestor != null) {
181 appIdNamespace = ancestor.getAppIdNamespace();
182 } else {
183 appIdNamespace = new AppIdNamespace(DatastoreApiHelper.getCurrentAppId(), "");
189 * Create a new {@link Query} that finds {@link Entity} objects with
190 * the specified {@code Key} as an ancestor.
192 * @param ancestor the ancestor key or null
193 * @throws IllegalArgumentException If ancestor is not complete.
195 public Query(Key ancestor) {
196 this(null, ancestor);
200 * Create a new {@link Query} that finds {@link Entity} objects with
201 * the specified {@code kind} and the specified {@code ancestor}. Note that
202 * kindless queries are not yet supported in the Java dev appserver.
204 * @param kind the kind or null to create a kindless query
205 * @param ancestor the ancestor key or null
206 * @throws IllegalArgumentException If the ancestor is not complete.
208 public Query(String kind, Key ancestor) {
209 this(kind, ancestor, null, false, DatastoreApiHelper.getCurrentAppIdNamespace(), false);
213 * Only {@link Entity} objects whose kind matches this value will be
214 * returned.
216 public String getKind() {
217 return kind;
221 * Returns the AppIdNamespace that is being queried.
222 * <p>The AppIdNamespace is set at construction time of this
223 * object using the {@link NamespaceManager} to retrieve the
224 * current namespace.
226 AppIdNamespace getAppIdNamespace() {
227 return appIdNamespace;
231 * Returns the appId for this {@link Query}.
233 public String getAppId() {
234 return appIdNamespace.getAppId();
238 * Returns the namespace for this {@link Query}.
240 public String getNamespace() {
241 return appIdNamespace.getNamespace();
245 * Gets the current ancestor for this query, or null if there is no
246 * ancestor specified.
248 public Key getAncestor() {
249 return ancestor;
253 * Sets an ancestor for this query.
255 * This restricts the query to only return result entities that are
256 * descended from a given entity. In other words, all of the results
257 * will have the ancestor as their parent, or parent's parent, or
258 * etc.
260 * If null is specified, unsets any previously-set ancestor. Passing
261 * {@code null} as a parameter does not query for entities without
262 * ancestors (this type of query is not currently supported).
264 * @return {@code this} (for chaining)
266 * @throws IllegalArgumentException If the ancestor key is incomplete, or if
267 * you try to unset an ancestor and have not set a kind, or if you try to
268 * unset an ancestor and have not previously set an ancestor.
270 public Query setAncestor(Key ancestor) {
271 if (ancestor != null && !ancestor.isComplete()) {
272 throw new IllegalArgumentException(ancestor + " is incomplete.");
273 } else if (ancestor == null) {
274 if (this.ancestor == null) {
275 throw new IllegalArgumentException(
276 "Cannot clear ancestor unless ancestor has already been set");
279 if (ancestor != null) {
280 if (!ancestor.getAppIdNamespace().equals(appIdNamespace)) {
281 throw new IllegalArgumentException(
282 "Namespace of ancestor key and query must match.");
285 this.ancestor = ancestor;
286 return this;
290 * @param distinct if this query should be distinct. This may only be used
291 * when the query has a projection.
292 * @return {@code this} (for chaining)
294 public Query setDistinct(boolean distinct) {
295 this.distinct = distinct;
296 return this;
300 * @return if this query is distinct
301 * @see #setDistinct(boolean)
303 public boolean getDistinct() {
304 return distinct;
308 * @param filter the filter to use for this query, or {@code null}
309 * @return {@code this} (for chaining)
310 * @see CompositeFilter
311 * @see FilterPredicate
313 public Query setFilter(Filter filter) {
314 this.filter = filter;
315 return this;
319 * @return the filter for this query or {@code null}
320 * @see #setFilter(Filter)
322 public Filter getFilter() {
323 return filter;
327 * Add a {@link FilterPredicate} on the specified property.
329 * <p>All {@link FilterPredicate}s added using this message are combined using
330 * {@link CompositeFilterOperator#AND}.
332 * <p>Cannot be used in conjunction with {@link #setFilter(Filter)} which sets
333 * a single {@link Filter} instead of many {@link FilterPredicate}s.
335 * @param propertyName The name of the property to which the filter applies.
336 * @param operator The filter operator.
337 * @param value An instance of a supported datastore type. Note that
338 * entities with multi-value properties identified by {@code propertyName}
339 * will match this filter if the multi-value property has at least one
340 * value that matches the condition expressed by {@code operator} and
341 * {@code value}. For more information on multi-value property filtering
342 * please see the
343 * <a href="http://cloud.google.com/appengine/docs/java/datastore">
344 * datastore documentation</a>.
346 * @return {@code this} (for chaining)
348 * @throws NullPointerException If {@code propertyName} or {@code operator}
349 * is null.
350 * @throws IllegalArgumentException If {@code value} is not of a
351 * type supported by the datastore. See
352 * {@link DataTypeUtils#isSupportedType(Class)}. Note that unlike
353 * {@link Entity#setProperty(String, Object)}, you cannot provide
354 * a {@link Collection} containing instances of supported types
355 * to this method.
356 * @deprecated Use {@link #setFilter(Filter)}
358 @Deprecated
359 public Query addFilter(String propertyName, FilterOperator operator, Object value) {
360 filterPredicates.add(operator.of(propertyName, value));
361 return this;
365 * Returns a mutable list of the current filter predicates.
367 * @see #addFilter(String, FilterOperator, Object)
368 * @deprecated Use {@link #setFilter(Filter)} and {@link #getFilter()} instead
370 @Deprecated
371 public List<FilterPredicate> getFilterPredicates() {
372 return filterPredicates;
376 * Specify how the query results should be sorted.
378 * The first call to addSort will register the property that will
379 * serve as the primary sort key. A second call to addSort will set
380 * a secondary sort key, etc.
382 * This method will always sort in ascending order. To control the
383 * order of the sort, use {@link #addSort(String,SortDirection)}.
385 * Note that entities with multi-value properties identified by
386 * {@code propertyName} will be sorted by the smallest value in the list.
387 * For more information on sorting properties with multiple values please see
388 * the <a href="http://cloud.google.com/appengine/docs/java/datastore">
389 * datastore documentation</a>.
391 * @return {@code this} (for chaining)
393 * @throws NullPointerException If any argument is null.
395 public Query addSort(String propertyName) {
396 return addSort(propertyName, SortDirection.ASCENDING);
400 * Specify how the query results should be sorted.
402 * The first call to addSort will register the property that will
403 * serve as the primary sort key. A second call to addSort will set
404 * a secondary sort key, etc.
406 * Note that if {@code direction} is {@link SortDirection#ASCENDING},
407 * entities with multi-value properties identified by
408 * {@code propertyName} will be sorted by the smallest value in the list. If
409 * {@code direction} is {@link SortDirection#DESCENDING}, entities with
410 * multi-value properties identified by {@code propertyName} will be sorted
411 * by the largest value in the list. For more information on sorting
412 * properties with multiple values please see
413 * the <a href="http://cloud.google.com/appengine/docs/java/datastore">
414 * datastore documentation</a>.
416 * @return {@code this} (for chaining)
418 * @throws NullPointerException If any argument is null.
420 public Query addSort(String propertyName, SortDirection direction) {
421 sortPredicates.add(new SortPredicate(propertyName, direction));
422 return this;
426 * Returns a mutable list of the current sort predicates.
428 public List<SortPredicate> getSortPredicates() {
429 return sortPredicates;
433 * Makes this query fetch and return only keys, not full entities.
435 * @return {@code this} (for chaining)
437 public Query setKeysOnly() {
438 keysOnly = true;
439 return this;
443 * Clears the keys only flag.
445 * @see #setKeysOnly()
446 * @return {@code this} (for chaining)
448 public Query clearKeysOnly() {
449 keysOnly = false;
450 return this;
454 * Adds a projection for this query.
456 * <p>Projections are limited in the following ways:
457 * <ul>
458 * <li>Un-indexed properties cannot be projected and attempting to do so will
459 * result in no entities being returned.
460 * <li>Projection {@link Projection#getName() names} must be unique.
461 * <li>Properties that have an equality filter on them cannot be projected. This includes
462 * the operators {@link FilterOperator#EQUAL} and {@link FilterOperator#IN}.
463 * </ul>
465 * @see #getProjections()
466 * @param projection the projection to add
467 * @return {@code this} (for chaining)
468 * @throws IllegalArgumentException if the query already contains a projection with the same name
470 public Query addProjection(Projection projection) {
471 Preconditions.checkArgument(!projectionMap.containsKey(projection.getName()),
472 "Query already contains projection with name: " + projection.getName());
473 projectionMap.put(projection.getName(), projection);
474 return this;
478 * Returns a mutable collection properties included in the projection for this query.
480 * <p>If empty, the full or keys only entities are returned. Otherwise
481 * partial entities are returned. A non-empty projection is not compatible
482 * with setting keys-only. In this case a {@link IllegalArgumentException}
483 * will be thrown when the query is {@link DatastoreService#prepare(Query) prepared}.
485 * <p>Projection queries are similar to SQL statements of the form:
486 * <pre>SELECT prop1, prop2, ...</pre>
487 * As they return partial entities, which only contain the properties
488 * specified in the projection. However, these entities will only contain
489 * a single value for any multi-valued property and, if a multi-valued
490 * property is specified in the order, an inequality property, or the
491 * projected properties, the entity will be returned multiple times.
492 * Once for each unique combination of values.
494 * <p>Specifying a projection:
495 * <ul>
496 * <li>May change the type of any property returned in a projection.
497 * <li>May change the index requirements for the given query.
498 * <li>Will cause a partial entity to be returned.
499 * <li>Will cause only entities that contain those properties to be returned.
500 * </ul>
501 * However, projection queries are significantly faster than normal queries.
503 * @return a mutable collection properties included in the projection for this query
504 * @see #addProjection(Projection)
506 public Collection<Projection> getProjections() {
507 return projectionMap.values();
511 * Returns true if this query will fetch and return keys only, false if it
512 * will fetch and return full entities.
514 public boolean isKeysOnly() {
515 return keysOnly;
519 * Creates a query sorted in the exact opposite direction as the current one.
521 * This function requires a sort order on {@link Entity#KEY_RESERVED_PROPERTY}
522 * to guarantee that each entity is uniquely identified by the set of properties
523 * used in the sort (which is required to exactly reverse the order of a query).
524 * Advanced users can reverse the sort orders manually if they know the set of
525 * sorted properties meets this requirement without a order on {@link
526 * Entity#KEY_RESERVED_PROPERTY}.
528 * The results of the reverse query will be the same as the results of the forward
529 * query but in reverse direction.
531 * {@link Cursor Cursors} from the original query may also be used in the
532 * reverse query.
534 * @return A new query with the sort order reversed.
535 * @throws IllegalStateException if the current query is not sorted by
536 * {@link Entity#KEY_RESERVED_PROPERTY}.
538 public Query reverse() {
539 List<SortPredicate> order = new ArrayList<SortPredicate>(sortPredicates.size());
540 boolean hasKeyOrder = false;
541 for (SortPredicate sort : sortPredicates) {
542 if (Entity.KEY_RESERVED_PROPERTY.equals(sort.getPropertyName())) {
543 hasKeyOrder = true;
545 if (!distinct || projectionMap.containsKey(sort.getPropertyName())) {
546 order.add(sort.reverse());
547 } else {
548 order.add(sort);
551 Preconditions.checkState(hasKeyOrder, "A sort on " + Entity.KEY_RESERVED_PROPERTY +
552 " is required to reverse a Query");
554 return new Query(kind, ancestor, order, filter, filterPredicates, keysOnly, appIdNamespace,
555 projectionMap.values(), distinct);
558 @Override
559 public boolean equals(Object o) {
560 if (this == o) {
561 return true;
563 if (o == null || getClass() != o.getClass()) {
564 return false;
567 Query query = (Query) o;
569 if (keysOnly != query.keysOnly) {
570 return false;
573 if (!Objects.equals(ancestor, query.ancestor)) {
574 return false;
576 if (!appIdNamespace.equals(query.appIdNamespace)) {
577 return false;
579 if (!Objects.equals(filter, query.filter)) {
580 return false;
582 if (!filterPredicates.equals(query.filterPredicates)) {
583 return false;
585 if (!Objects.equals(kind, query.kind)) {
586 return false;
588 if (!sortPredicates.equals(query.sortPredicates)) {
589 return false;
591 if (!projectionMap.equals(query.projectionMap)) {
592 return false;
594 if (distinct != query.distinct) {
595 return false;
598 return true;
601 @Override
602 public int hashCode() {
603 return Objects.hash(kind, sortPredicates, filterPredicates, filter, ancestor, keysOnly,
604 appIdNamespace, projectionMap, distinct);
608 * Obtains a hash code of the {@code Query} ignoring any 'value' arguments associated with
609 * any query filters.
611 int hashCodeNoFilterValues() {
612 int filterHashCode = 1;
613 for (FilterPredicate filterPred : filterPredicates) {
614 filterHashCode = 31 * filterHashCode + filterPred.hashCodeNoFilterValues();
616 filterHashCode = 31 * filterHashCode + ((filter == null) ? 0 : filter.hashCodeNoFilterValues());
617 return Objects.hash(kind, sortPredicates, filterHashCode, ancestor, keysOnly,
618 appIdNamespace, projectionMap, distinct);
622 * Outputs a SQL like string representing the query.
624 @Override
625 public String toString() {
626 StringBuilder result = new StringBuilder("SELECT ");
627 if (distinct) {
628 result.append("DISTINCT ");
630 if (!projectionMap.isEmpty()) {
631 Joiner.on(", ").appendTo(result, projectionMap.values());
632 } else if (keysOnly) {
633 result.append("__key__");
634 } else {
635 result.append('*');
638 if (kind != null) {
639 result.append(" FROM ");
640 result.append(kind);
643 if (ancestor != null || !filterPredicates.isEmpty() || filter != null) {
644 result.append(" WHERE ");
645 final String AND_SEPARATOR = " AND ";
647 if (filter != null) {
648 result.append(filter);
649 } else if (!filterPredicates.isEmpty()) {
650 Joiner.on(AND_SEPARATOR).appendTo(result, filterPredicates);
653 if (ancestor != null) {
654 if (!filterPredicates.isEmpty() || filter != null) {
655 result.append(AND_SEPARATOR);
657 result.append("__ancestor__ is ");
658 result.append(ancestor);
662 if (!sortPredicates.isEmpty()) {
663 result.append(" ORDER BY ");
664 Joiner.on(", ").appendTo(result, sortPredicates);
666 return result.toString();
670 * SortPredicate is a data container that holds a single sort
671 * predicate.
673 public static final class SortPredicate implements Serializable {
674 @SuppressWarnings("hiding")
675 static final long serialVersionUID = -623786024456258081L;
676 private final String propertyName;
677 private final SortDirection direction;
679 public SortPredicate(String propertyName, SortDirection direction) {
680 if (propertyName == null) {
681 throw new NullPointerException("Property name was null");
684 if (direction == null) {
685 throw new NullPointerException("Direction was null");
688 this.propertyName = propertyName;
689 this.direction = direction;
693 * @return A sort predicate with the direction reversed.
695 public SortPredicate reverse() {
696 return new SortPredicate(propertyName,
697 direction == SortDirection.ASCENDING ?
698 SortDirection.DESCENDING : SortDirection.ASCENDING);
702 * Gets the name of the property to sort on.
704 public String getPropertyName() {
705 return propertyName;
709 * Gets the direction of the sort.
711 public SortDirection getDirection() {
712 return direction;
715 @Override
716 public boolean equals(Object o) {
717 if (this == o) {
718 return true;
720 if (o == null || getClass() != o.getClass()) {
721 return false;
724 SortPredicate that = (SortPredicate) o;
726 if (direction != that.direction) {
727 return false;
729 if (!propertyName.equals(that.propertyName)) {
730 return false;
733 return true;
736 @Override
737 public int hashCode() {
738 int result;
739 result = propertyName.hashCode();
740 result = 31 * result + direction.hashCode();
741 return result;
744 @Override
745 public String toString() {
746 return propertyName + (direction == SortDirection.DESCENDING ? " DESC" : "");
751 * The base class for a query filter.
753 * All sub classes should be immutable.
755 public abstract static class Filter implements Serializable {
756 @SuppressWarnings("hiding")
757 static final long serialVersionUID = -845113806195204425L;
759 Filter() {}
762 * Obtains the hash code for the {@code Filter} ignoring any value parameters associated
763 * with the filter.
765 abstract int hashCodeNoFilterValues();
769 * A {@link Filter} that combines several sub filters using a {@link CompositeFilterOperator}.
771 * For example, to construct a filter of the form <code>a = 1 AND (b = 2 OR c = 3)</code> use:
772 * <pre> {@code
773 * new CompositeFilter(CompositeFilterOperator.AND, Arrays.asList(
774 * new FilterPredicate("a", FilterOperator.EQUAL, 1),
775 * new CompositeFilter(CompositeFilterOperator.OR, Arrays.<Filter>asList(
776 * new FilterPredicate("b", FilterOperator.EQUAL, 2),
777 * new FilterPredicate("c", FilterOperator.EQUAL, 3)))));
778 * }</pre>
779 * or
780 * <pre> {@code
781 * CompositeFilterOperator.and(
782 * FilterOperator.EQUAL.of("a", 1),
783 * CompositeFilterOperator.or(
784 * FilterOperator.EQUAL.of("b", 2),
785 * FilterOperator.EQUAL.of("c", 3)));
786 * }</pre>
788 public static final class CompositeFilter extends Filter {
789 @SuppressWarnings("hiding")
790 static final long serialVersionUID = 7930286402872420509L;
792 private final CompositeFilterOperator operator;
793 private final ImmutableList<Filter> subFilters;
795 public CompositeFilter(CompositeFilterOperator operator, Collection<Filter> subFilters) {
796 Preconditions.checkArgument(subFilters.size() >= 2, "At least two sub filters are required.");
797 this.operator = checkNotNull(operator);
798 this.subFilters = ImmutableList.copyOf(subFilters);
802 * @return the operator
804 public CompositeFilterOperator getOperator() {
805 return operator;
809 * @return an immutable list of sub filters
811 public List<Filter> getSubFilters() {
812 return subFilters;
815 @Override
816 public String toString() {
817 StringBuilder builder = new StringBuilder();
818 builder.append('(');
819 Joiner.on(" " + operator + " ").appendTo(builder, subFilters);
820 builder.append(')');
821 return builder.toString();
824 @Override
825 public int hashCode() {
826 return Objects.hash(operator, subFilters);
829 @Override
830 int hashCodeNoFilterValues() {
831 int result = 1;
832 result = 31 * result + operator.hashCode();
833 for (Filter filter : subFilters) {
834 result = 31 * result + filter.hashCodeNoFilterValues();
836 return result;
839 @Override
840 public boolean equals(Object obj) {
841 if (this == obj) return true;
842 if (!(obj instanceof CompositeFilter)) return false;
843 CompositeFilter other = (CompositeFilter) obj;
844 if (operator != other.operator) return false;
845 return subFilters.equals(other.subFilters);
850 * A {@link Filter} on a single property.
852 public static final class FilterPredicate extends Filter {
853 @SuppressWarnings("hiding")
854 static final long serialVersionUID = 7681475799401864259L;
855 private final String propertyName;
856 private final FilterOperator operator;
857 private final Object value;
860 * Constructs a filter predicate from the given parameters.
862 * @param propertyName the name of the property on which to filter
863 * @param operator the operator to apply
864 * @param value A single instances of a supported type or if {@code
865 * operator} is {@link FilterOperator#IN} a non-empty {@link Iterable}
866 * object containing instances of supported types.
868 * @throws IllegalArgumentException If the provided filter values are not
869 * supported.
871 * @see DataTypeUtils#isSupportedType(Class)
874 public FilterPredicate(String propertyName, FilterOperator operator, Object value) {
875 if (propertyName == null) {
876 throw new NullPointerException("Property name was null");
877 } else if (operator == null) {
878 throw new NullPointerException("Operator was null");
879 } else if (operator == FilterOperator.IN) {
880 if (!(value instanceof Collection<?>) && value instanceof Iterable<?>) {
881 List<Object> newValue = new ArrayList<Object>();
882 Iterables.addAll(newValue, (Iterable<?>) value);
883 value = newValue;
885 DataTypeUtils.checkSupportedValue(propertyName, value, true, true);
886 } else {
887 DataTypeUtils.checkSupportedValue(propertyName, value, false, false);
889 this.propertyName = propertyName;
890 this.operator = operator;
891 this.value = value;
895 * Gets the name of the property to be filtered on.
897 public String getPropertyName() {
898 return propertyName;
902 * Gets the operator describing how to apply the filter.
904 public FilterOperator getOperator() {
905 return operator;
909 * Gets the argument to the filter operator.
911 public Object getValue() {
912 return value;
915 @Override
916 public boolean equals(Object o) {
917 if (this == o) {
918 return true;
920 if (o == null || getClass() != o.getClass()) {
921 return false;
924 FilterPredicate that = (FilterPredicate) o;
926 if (operator != that.operator) {
927 return false;
929 if (!propertyName.equals(that.propertyName)) {
930 return false;
932 if (!Objects.equals(value, that.value)) {
933 return false;
936 return true;
939 @Override
940 public int hashCode() {
941 return Objects.hash(propertyName, operator, value);
944 @Override
945 int hashCodeNoFilterValues() {
946 return Objects.hash(propertyName, operator);
949 @Override
950 public String toString() {
951 return propertyName + " " + operator + " " + (value != null ? value : "NULL");
956 * A {@link Filter} representing a geo-region containment predicate.
958 public static final class StContainsFilter extends Filter {
959 private static final long serialVersionUID = 1L;
960 private final String propertyName;
961 private final GeoRegion region;
964 * Constructs a geo-region filter from the given arguments.
966 * @param propertyName the name of the property on which to test containment
967 * @param region a geo-region object against which to test the property value
969 public StContainsFilter(String propertyName, GeoRegion region) {
970 this.propertyName = checkNotNull(propertyName);
971 this.region = checkNotNull(region);
974 public String getPropertyName() {
975 return propertyName;
978 public GeoRegion getRegion() {
979 return region;
982 @Override
983 public boolean equals(Object o) {
984 if (this == o) {
985 return true;
987 if (o == null || getClass() != o.getClass()) {
988 return false;
991 StContainsFilter that = (StContainsFilter) o;
993 if (!propertyName.equals(that.propertyName)) {
994 return false;
996 if (!region.equals(that.region)) {
997 return false;
999 return true;
1002 @Override
1003 public int hashCode() {
1004 return Objects.hash(propertyName, region);
1007 @Override
1008 int hashCodeNoFilterValues() {
1009 return propertyName.hashCode();
1012 @Override
1013 public String toString() {
1014 return String.format("StContainsFilter [%s: %s]", propertyName, region);
1019 * A geographic region intended for use in an
1020 * {@link StContainsFilter}. Note that this is the only purpose for
1021 * which it should be used: in particular, it is not suitable as a
1022 * Property value to be stored in Datastore.
1024 public abstract static class GeoRegion implements Serializable {
1026 GeoRegion() {}
1029 * Determines whether the given {@link GeoPt} value lies within this
1030 * geographic region. If the point lies on the border of the region
1031 * it is considered to be contained.
1033 public abstract boolean contains(GeoPt point);
1036 * A geographical region representing all points within a fixed
1037 * distance from a central point, i.e., a circle. Intended for use in
1038 * a geo-containment predicate filter.
1040 public static final class Circle extends GeoRegion {
1041 private static final long serialVersionUID = 1L;
1043 private final GeoPt center;
1044 private final double radius;
1047 * Creates a new {@code Circle} object from the given arguments.
1049 * @param center a {@code GeoPt} representing the center of the circle
1050 * @param radius the radius of the circle, expressed in meters
1052 public Circle(GeoPt center, double radius) {
1053 this.center = checkNotNull(center);
1054 checkArgument(radius >= 0);
1055 this.radius = radius;
1058 @Override
1059 public boolean contains(GeoPt point) {
1060 return GeoPt.distance(center, point) <= radius;
1063 public GeoPt getCenter() {
1064 return center;
1067 public double getRadius() {
1068 return radius;
1071 @Override
1072 public boolean equals(Object o) {
1073 if (this == o) {
1074 return true;
1077 if (o == null || getClass() != o.getClass()) {
1078 return false;
1081 Circle other = (Circle) o;
1083 if (!center.equals(other.center)) {
1084 return false;
1086 if (Double.compare(radius, other.radius) != 0) {
1087 return false;
1090 return true;
1093 @Override
1094 public int hashCode() {
1095 return Objects.hash(center, radius);
1098 @Override
1099 public String toString() {
1100 return String.format("Circle [(%s),%f]", center, radius);
1105 * A simple geographical region bounded by two latitude lines, and two
1106 * longitude lines, i.e., a "rectangle". It's not really a rectangle,
1107 * of course, because longitude lines are not really parallel.
1108 * <p>
1109 * Intended for use in a geo-containment predicate filter.
1111 public static final class Rectangle extends GeoRegion {
1112 private static final long serialVersionUID = 1L;
1113 private final GeoPt southwest;
1114 private final GeoPt northeast;
1116 public Rectangle(GeoPt southwest, GeoPt northeast) {
1117 checkNotNull(southwest);
1118 checkNotNull(northeast);
1119 checkArgument(southwest.getLatitude() <= northeast.getLatitude());
1120 this.southwest = southwest;
1121 this.northeast = northeast;
1124 public GeoPt getSouthwest() {
1125 return southwest;
1128 public GeoPt getNortheast() {
1129 return northeast;
1132 @Override
1133 public boolean contains(GeoPt point) {
1134 if (point.getLatitude() > northeast.getLatitude()
1135 || point.getLatitude() < southwest.getLatitude()) {
1136 return false;
1139 if (southwest.getLongitude() > northeast.getLongitude()) {
1140 return point.getLongitude() <= northeast.getLongitude()
1141 || point.getLongitude() >= southwest.getLongitude();
1143 return southwest.getLongitude() <= point.getLongitude()
1144 && point.getLongitude() <= northeast.getLongitude();
1147 @Override
1148 public boolean equals(Object o) {
1149 if (this == o) {
1150 return true;
1153 if (o == null || getClass() != o.getClass()) {
1154 return false;
1157 Rectangle other = (Rectangle) o;
1159 if (!southwest.equals(other.southwest)) {
1160 return false;
1162 if (!northeast.equals(other.northeast)) {
1163 return false;
1166 return true;
1169 @Override
1170 public int hashCode() {
1171 return Objects.hash(southwest, northeast);
1174 @Override
1175 public String toString() {
1176 return String.format("Rectangle [(%s),(%s)]", southwest, northeast);