All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.stackmob.sdk.api.StackMobQuery Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2011 StackMob
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.stackmob.sdk.api;

import com.stackmob.sdk.util.ListHelpers;

import java.util.*;

/**
 * A class that builds queries for data access methods like those in {@link StackMob}. Example usage:
 * 
 * {@code
 *     StackMobQuery query = new StackMobQuery("user")
 *                             .fieldIsGreaterThan("age", 20)
 *                             .fieldIsLessThanOrEqualTo("age", 40)
 *                             .fieldIsIn("friend", Arrays.asList("joe", "bob", "alice");
 *
 *     //Of if you have multiple constraints on the same field and you want to type less
 *     StackMobQuery query = new StackMobQuery("user")
 *                             .field(new StackMobQueryField("age").isGreaterThan(20).isLessThanOrEqualTo(40))
 *                             .field(new StackMobQueryField("friend").isIn(Arrays.asList("joe", "bob", "alice")));
 * }
 * 
* * A few helpful notes about this object: *
    *
  • this class is not thread safe. make sure to synchronize all calls
  • *
  • the {@link #field(StackMobQueryField)} method helps you build up part of your query on a specific field
  • *
  • you can only operate on one field at a time, but you can call field("field") as many times as you want
  • *
  • * you can call methods like fieldIsGreaterThan("field", "value") or fieldIsLessThanOrEqualTo("field", "value") directly on a StackMobQuery object. *
  • *
*/ public class StackMobQuery { private String objectName; private Map headers = new HashMap(); private Map args = new HashMap(); private int orCount = 1; private int andCount = 1; private boolean isAnd = false; private boolean isOr = false; private static final String OrFormat = "[or%d]."; private static final String AndFormat = "[and%d]."; private static final String RangeHeader = "Range"; private static final String OrderByHeader = "X-StackMob-OrderBy"; /** * Represents ascending or descending order */ public static enum Ordering { DESCENDING("desc"), ASCENDING("asc"); private String name; Ordering(String name) { this.name = name; } public String toString() { return name; } } static enum Operator { LT("lt"), GT("gt"), LTE("lte"), GTE("gte"), IN("in"), NIN("nin"), NEAR("near"), WITHIN("within"), NE("ne"), NULL("null"), EMPTY("empty"); private String operator; Operator(String operator) { this.operator = operator; } public String getOperatorForURL() { return "["+operator+"]"; } } /** * creates an empty query. {@link #StackMobQuery(String)} should be used in almost all cases. Only use * this if you then later call {@link #setObjectName(String)} */ public StackMobQuery() {} /** * create a query on a specific object * @param objectName the schema you're querying against */ public StackMobQuery(String objectName) { this.objectName = objectName; } /** * create a query on a specific object * @param objectName the schema you're querying against * @return the new query */ public static StackMobQuery objects(String objectName) { return new StackMobQuery(objectName); } /** * set the schema being queried against * @param objectName the schema */ public void setObjectName(String objectName) { this.objectName = objectName; } /** * get the schema being queried against * @return objectName the schema */ public String getObjectName() { return objectName; } /** * get the headers generated by this query * @return headers */ public Map getHeaders() { return this.headers; } /** * get the arguments generated by this query * @return arguments */ public List> getArguments() { //If the top level was marked specifically as OR, we need to add an extra layer Map finalMap = isOr ? prependString(String.format(OrFormat, orCount), this.args) : this.args; return new ArrayList>(finalMap.entrySet()); } private Map getNestedArguments() { return this.args; } /** * copy the constraints in a give query to this one * @param other the query to copy * @return a new query contain all the constraints of both */ public StackMobQuery add(StackMobQuery other) { this.headers.putAll(other.headers); this.args.putAll(other.args); return this; } private Map prependString(String prefix, Map args) { Map newMap = new HashMap(); for(String arg : args.keySet()) { newMap.put(prefix + arg, args.get(arg)); } return newMap; } /** * Separate two constraints with an AND. Constraints are separated * like this by default, but this allows your queries to read naturally. * You cannot mix AND and OR at the same level of a query; instead you need to * start a sub-expression with {@link #and(StackMobQuery clauses)}, * effectively adding parenthesis * @return the query ready to accept another statement */ public StackMobQuery and() { if(this.isOr) throw new IllegalStateException("Mixing OR and AND on the same level is not allowed"); this.isAnd = true; return this; } /** * Add a new parenthesized expression to be joined by AND with the * other constraints in the query. The sub-expression will be treated * as the logical OR of a set of clauses, giving you A && (B || C || ...) * StackMob only supports one such parenthesized OR block * in a query, but it can contain any number of clauses. * @param clauses a sub-query. Each constraint added to it is combined into an OR statement. * @return A new query with the OR statement joined with AND to the rest of the clauses. */ public StackMobQuery and(StackMobQuery clauses) { if(this.isOr) throw new IllegalStateException("Mixing OR and AND on the same level is not allowed"); this.isAnd = true; String orString = String.format(OrFormat, orCount); for(Map.Entry arg : prependString(orString, clauses.getNestedArguments()).entrySet()) { this.args.put(arg.getKey(), arg.getValue()); } orCount++; return this; } /** * Separate two constraints with an OR. This can be done at the top level * of a query, or within a sub-query added with {@link #and(StackMobQuery clauses)} * You cannot mix AND and OR at the same level of a query; instead you need to * start a sub-expression with ,{@link #or(StackMobQuery clauses)} * effectively adding parenthesis * @return the query ready to accept another statement */ public StackMobQuery or() { if(this.isAnd) throw new IllegalStateException("Mixing OR and AND on the same level is not allowed"); this.isOr = true; return this; } /** * Add a new parenthesized expression to be joined by OR with the * other constraints in the query. The sub-expression will be treated * as the logical AND of a set of clauses, giving you A || (B && C && ...) * Redundant nested AND statements will be rejected. * @param clauses a sub-query. Each constraint added to it is combined into an AND statement. * @return A new query with the AND statement joined with OR to the rest of the clauses */ public StackMobQuery or(StackMobQuery clauses) { if(this.isAnd) throw new IllegalStateException("Mixing OR and AND on the same level is not allowed"); this.isOr = true; String andString = String.format(AndFormat, andCount); for(Map.Entry arg : prependString(andString, clauses.getNestedArguments()).entrySet()) { this.args.put(arg.getKey(), arg.getValue()); } andCount++; return this; } /** * Begin adding constraints to a field via a {@link StackMobQueryField} * @param field the name of the field * @return a query specific to that field */ public StackMobQuery field(StackMobQueryField field) { add(field.getQuery()); return this; } /** * add a "NEAR" to your query for the given StackMobGeoPoint field. Query results are automatically returned * sorted by distance closest to the queried point * @param field the StackMobGeoPoint field whose value to test * @param point the lon/lat location to center the search * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsNear(String field, StackMobGeoPoint point) { return putInMap(field, Operator.NEAR, ListHelpers.join(point.asList(), ",")); } /** * add a "NEAR" to your query for the given StackMobGeoPoint field. Query results are automatically returned * sorted by distance closest to the queried point * @param field the StackMobGeoPoint field whose value to test * @param point the lon/lat location to center the search * @param maxDistanceMi the maximum distance in miles a matched field can be from point. * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsNearWithinMi(String field, StackMobGeoPoint point, Double maxDistanceMi) { List arguments = point.asList(); arguments.add(StackMobGeoPoint.miToRadians(maxDistanceMi).toString()); //convert to radians return putInMap(field, Operator.NEAR, ListHelpers.join(arguments, ",")); } /** * add a "NEAR" to your query for the given StackMobGeoPoint field. Query results are automatically returned * sorted by distance closest to the queried point * @param field the StackMobGeoPoint field whose value to test * @param point the lon/lat location to center the search * @param maxDistanceKm the maximum distance in kilometers a matched field can be from point. * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsNearWithinKm(String field, StackMobGeoPoint point, Double maxDistanceKm) { List arguments = point.asList(); arguments.add(StackMobGeoPoint.kmToRadians(maxDistanceKm).toString()); //convert to radians return putInMap(field, Operator.NEAR, ListHelpers.join(arguments, ",")); } /** * add a "WITHIN" to your query for the given StackMobGeoPoint field. Query results are not sorted by distance. * @param field the StackMobGeoPoint field whose value to test * @param point the lon/lat location to center the search * @param radiusInMi the maximum distance in miles a matched field can be from point. * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsWithinRadiusInMi(String field, StackMobGeoPoint point, Double radiusInMi) { List arguments = point.asList(); arguments.add(StackMobGeoPoint.miToRadians(radiusInMi).toString()); //convert to radians return putInMap(field, Operator.WITHIN, ListHelpers.join(arguments, ",")); } /** * add a "WITHIN" to your query for the given StackMobGeoPoint field. Query results are not sorted by distance. * @param field the StackMobGeoPoint field whose value to test * @param point the lon/lat location to center the search * @param radiusInKm the maximum distance in kilometers a matched field can be from point. * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsWithinRadiusInKm(String field, StackMobGeoPoint point, Double radiusInKm) { List arguments = point.asList(); arguments.add(StackMobGeoPoint.kmToRadians(radiusInKm).toString()); //convert to radians return putInMap(field, Operator.WITHIN, ListHelpers.join(arguments, ",")); } /** * add a "WITHIN" to your query for the given StackMobGeoPoint field. Matched fields will be within the 2-dimensional bounds * defined by the lowerLeft and upperRight StackMobGeoPoints given * @param field the StackMobGeoPoint field whose value to test * @param lowerLeft the lon/lat location of the lower left corner of the bounding box * @param upperRight the lon/lat location of the upper right corner of the bounding box * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsWithinBox(String field, StackMobGeoPoint lowerLeft, StackMobGeoPoint upperRight) { List arguments = lowerLeft.asList(); arguments.addAll(upperRight.asList()); return putInMap(field, Operator.WITHIN, ListHelpers.join(arguments, ",")); } /** * add an "IN" to your query. test whether the given field's value is in the given list of possible values * @param field the field whose value to test * @param values the values against which to match * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsIn(String field, List values) { return putInMap(field, Operator.IN, ListHelpers.join(values, ",")); } /** * add a "NIN" to your query. test whether the given field's value is not in the given list of possible values * @param field the field whose value to test * @param values the values against which to match * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsNotIn(String field, List values) { return putInMap(field, Operator.NIN, ListHelpers.join(values, ",")); } /** * add a "NE" to your query. test whether the given field's value is not equal to the given value * @param field the field whose value to test * @param val the value against which to match * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsNotEqual(String field, String val) { if(val == null) { return fieldIsNotNull(field); } if(val == "") { return putInMap(field, Operator.EMPTY, "false"); } return putInMap(field, Operator.NE, val); } /** * add a "NULL" to your query. test whether the given field's value is null * @param field the field whose value to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsNull(String field) { return putInMap(field, Operator.NULL, "true"); } /** * add a "NULL" to your query. test whether the given field's value is not null * @param field the field whose value to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsNotNull(String field) { return putInMap(field, Operator.NULL, "false"); } /** * same as {@link #fieldIsLessThan(String, String)}, except works with Strings * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsLessThan(String field, String val) { return putInMap(field, Operator.LT, val); } /** * same as {@link #fieldIsLessThan(String, String)}, except works with Strings * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsLessThan(String field, int val) { return putInMap(field, Operator.LT, String.valueOf(val)); } /** * same as {@link #fieldIsLessThan(String, String)}, except applies "<=" instead of "<" * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIslessThanOrEqualTo(String field, String val) { return putInMap(field, Operator.LTE, val); } /** * same as {@link #fieldIsLessThan(String, String)}, except applies "<=" instead of "<" * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsLessThanOrEqualTo(String field, int val) { return putInMap(field, Operator.LTE, String.valueOf(val)); } /** * same as {@link #fieldIsLessThan(String, String)}, except applies ">" instead of "<" * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsGreaterThan(String field, String val) { return putInMap(field, Operator.GT, val); } /** * same as {@link #fieldIsLessThan(String, String)}, except applies ">" instead of "<" * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsGreaterThan(String field, int val) { return putInMap(field, Operator.GT, String.valueOf(val)); } /** * same as {@link #fieldIsLessThan(String, String)}, except applies ">=" instead of "<" * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsGreaterThanOrEqualTo(String field, String val) { return putInMap(field, Operator.GTE, val); } /** * same as {@link #fieldIsLessThan(String, String)}, except applies ">=" instead of "<" * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsGreaterThanOrEqualTo(String field, int val) { return putInMap(field, Operator.GTE, String.valueOf(val)); } /** * add an "=" to your query. test whether the given field's value is equal to the given value * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsEqualTo(String field, String val) { if(val == null) { return fieldIsNull(field); } if(val == "") { return putInMap(field, Operator.EMPTY, "true"); } args.put(field, val); return this; } /** * add an "=" to your query. test whether the given field's value is equal to the given value * @param field the field whose value to test * @param val the value against which to test * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsEqualTo(String field, int val) { return fieldIsEqualTo(field, String.valueOf(val)); } /** * add an "ORDER BY" to your query * @param field the field to order by * @param ordering the ordering of that field * @return the new query that resulted from adding this operation */ public StackMobQuery fieldIsOrderedBy(String field, Ordering ordering) { String buf = headers.get(OrderByHeader); if(buf != null) { buf += ","; } else { buf = ""; } buf += field+":"+ordering.toString(); headers.put(OrderByHeader, buf); return this; } /** * this method lets you add a "LIMIT" and "SKIP" to your query at once. Can be used to implement pagination in your app. * @param start the starting object number (inclusive) * @param end the ending object number (inclusive) * @return the new query that resulted from adding this operation */ public StackMobQuery isInRange(Integer start, Integer end) { headers.put(RangeHeader, "objects="+start.toString()+"-"+end.toString()); return this; } /** * same thing as {@link #isInRange(Integer, Integer)}, except does not specify an end to the range. * instead, gets all objects from a starting point (including) * @param start the starting object number * @return the new query that resulted from adding this operation */ public StackMobQuery isInRange(Integer start) { headers.put(RangeHeader, "objects="+start.toString()+"-"); return this; } private StackMobQuery putInMap(String field, Operator operator, String value) { args.put(field+operator.getOperatorForURL(), value); return this; } private StackMobQuery putInMap(String field, Operator operator, int value) { putInMap(field, operator, Integer.toString(value)); return this; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy