/* Copyright (c) 2008 Google Inc. * * 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.google.gdata.client; import com.google.gdata.util.common.base.CharEscapers; import com.google.gdata.data.ICategory; import com.google.gdata.data.DateTime; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; /** * The Query class is a helper class that aids in the construction of a * GData query. It provides a simple API and object model that exposes * query parameters. Once constructed, the query can be executed against * a GData service. * <p> * The Query class also acts as a simple base class for GData services * that support custom query parameters. These services can subclass * the base Query class, add APIs to represent service query parameters, * and participate in the Query URI generation process. * * @see Service#query(Query, Class) * * */ public class Query { /** * Magic value indicating that a numeric field is not set. */ public static final int UNDEFINED = -1; /** * Defines all query return formats. Return format "json-xd" is * not supported. */ public static enum ResultFormat { DEFAULT("default"), // default for target resource (Atom or media) ATOM("atom"), RSS("rss"), JSON("json"), JSONC("jsonc"), ATOM_IN_SCRIPT("atom-in-script"), RSS_IN_SCRIPT("rss-in-script"), JSON_IN_SCRIPT("json-in-script"), JSONC_IN_SCRIPT("jsonc-in-script"), JSON_XD("json-xd"), ATOM_SERVICE("atom-service"); /** * Value to use for the "alt" param. */ private String paramValue; /** * Constructs a new ResultFormat object using a given value to use for the * "alt" parameter. * * @param value value to use for the "alt" parameter. */ private ResultFormat(String value) { this.paramValue = value; } /** * Returns the value to use for the "alt" parameter. * * @return value to use for the "alt" parameter. */ public String paramValue() { return paramValue; } } /** Base feed URL against which the query will be applied. */ private URL feedUrl; /** The list of category filters associate with the query. */ private List<CategoryFilter> categoryFilters = new LinkedList<CategoryFilter>(); /** Fields partial selection query parameter */ private String fields; /** Full-text search query string. */ private String queryString; /** Author name or e-mail address for matched entries. */ private String author; /** Minimum updated timestamp for matched entries. */ private DateTime updatedMin; /** Maximum updated timestamp for matched entries. */ private DateTime updatedMax; /** Minimum published timestamp for matched entries. */ private DateTime publishedMin; /** Maximum published timestamp for matched entries. */ private DateTime publishedMax; /** * The start index for query results. A value of {@link #UNDEFINED} * indicates that no start index has been set. */ private int startIndex = UNDEFINED; /** * The maximum number of results to return for the query. A value of * {@link #UNDEFINED} indicates the server can determine the maximum size. */ private int maxResults = UNDEFINED; /** * The expected result format for the query. The default is * {@link ResultFormat#DEFAULT}. */ private ResultFormat resultFormat = ResultFormat.DEFAULT; /** * The strictness of the query parameter parsing on the server. If strict * mode is enabled any unknown query parameters will be rejected. The * default value is false. */ private boolean strict = false; /** * The list of custom parameters associated with the query. */ private List<CustomParameter> customParameters = new ArrayList<CustomParameter>(); /** * Constructs a new Query object that targets a feed. The initial * state of the query contains no parameters, meaning all entries * in the feed would be returned if the query was executed immediately * after construction. * * @param feedUrl the URL of the feed against which queries will be * executed. */ public Query(URL feedUrl) { this.feedUrl = feedUrl; } /** * Returns the feed URL of this query. * * @return Feed URL. */ public URL getFeedUrl() { return feedUrl; } /** * Sets the "fields" partial selection query parameter. * * @param fields query value */ public void setFields(String fields) { this.fields = fields; } /** * Returns the fields query string that will be used for the query. */ public String getFields() { return fields; } /** * Sets the full text query string that will be used for the query. * * @param query the full text search query string. A value of * {@code null} disables full text search for this Query. */ public void setFullTextQuery(String query) { this.queryString = query; } /** * Returns the full text query string that will be used for the query. */ public String getFullTextQuery() { return queryString; } /** * The CategoryFilter class is used to define sets of category conditions * that must be met in order for an entry to match. * <p> * The CategoryFilter can contain multiple category criteria (inclusive * or exclusive). If it does contain multiple categories, then the * query matches if any one of the category filter criteria is met, * i.e. it is a logical 'OR' of the contained category criteria. * To match, an entry must contain at least one included category or * must not contain at least one excluded category. * <p> * It is also possible to add multiple CategoryFilters to a Query. In * this case, each individual CategoryFilter must be true for an entry * to match, i.e. it is a logical 'AND' of all CategoryFilters. * * @see Query#addCategoryFilter(CategoryFilter) */ public static class CategoryFilter { /** List of categories that returned entries must match. */ private final List<ICategory> categories; public List<ICategory> getCategories() { return categories; } /** List of categories that returned entries must match. */ private final List<ICategory> excludeCategories; public List<ICategory> getExcludeCategories() { return excludeCategories; } /** * Creates an empty category filter. */ public CategoryFilter() { categories = new LinkedList<ICategory>(); excludeCategories = new LinkedList<ICategory>(); } /** * Creates a new category filter using the supplied inclusion and * exclusion lists. A null value for either is equivalent to an * empty list. */ public CategoryFilter(List<ICategory> included, List<ICategory> excluded) { if (included != null) { categories = included; } else { categories = new LinkedList<ICategory>(); } if (excluded != null) { excludeCategories = excluded; } else { excludeCategories = new LinkedList<ICategory>(); } } /** * Creates a simple category filter containing only a single * {@link ICategory}. * * @param category an initial category to add to the filter. */ public CategoryFilter(ICategory category) { this(); categories.add(category); } /** * Adds a new {@link ICategory} to the query, indicating that entries * containing the category should be considered to match. * * @param category the category to add to query parameters. */ public void addCategory(ICategory category) { categories.add(category); } /** * Adds a new {@link ICategory} to the query, indicating that entries * that do not contain the category should be considered to * match. * * @param category the category to add to query parameters. */ public void addExcludeCategory(ICategory category) { excludeCategories.add(category); } private String getQueryString(ICategory category) { StringBuilder sb = new StringBuilder(); String scheme = category.getScheme(); if (scheme != null) { sb.append("{"); sb.append(scheme); sb.append("}"); } sb.append(category.getTerm()); return sb.toString(); } /** * Returns a string representation for the category conditions in * the CategoryFilter, in the format used by a Query URI. */ @Override public String toString() { StringBuilder sb = new StringBuilder(); boolean isFirst = true; for (ICategory category : categories) { if (isFirst) { isFirst = false; } else { sb.append("|"); } sb.append(getQueryString(category)); } for (ICategory category : excludeCategories) { if (isFirst) { isFirst = false; } else { sb.append("|"); } sb.append("-"); sb.append(getQueryString(category)); } return sb.toString(); } } /** * Adds a new CategoryFilter to the query. For an entry to match the * query criteria, it must match against <b>all</b> CategoryFilters * that have been associated with the query. */ public void addCategoryFilter(CategoryFilter categoryFilter) { categoryFilters.add(categoryFilter); } /** * Returns the current list of CategoryFilters associated with the query. * * @return list of category filters. */ public List<CategoryFilter> getCategoryFilters() { return Collections.unmodifiableList(categoryFilters); } /** * Sets the author name or email address used for the query. Only entries * with an author whose name or email address match the specified value * will be returned. * * @param author the name or email address for matched entries. A value of * {@code null} disables author-based matching. */ public void setAuthor(String author) { this.author = author; } /** * Returns the author name or email address used for the query. Only entries * with an author whose name or email address match the specified value * will be returned. * * @return the name or email address for matched entries. A value of * {@code null} means no author-based matching. */ public String getAuthor() { return this.author; } /** * Sets the minimum updated timestamp used for the query. Only entries with * an updated timestamp equal to or later than the specified timestamp will be * returned. * * @param updatedMin minimum updated timestamp for matched entries. A value * of {@code null} disables minimum timestamp filtering. */ public void setUpdatedMin(DateTime updatedMin) { this.updatedMin = updatedMin; } /** * Returns the minimum updated timestamp used for this query. Only entries * with an updated timestamp equal to or later than the specified timestamp * will be returned. * * @return minimum updated timestamp for matched entries. A value of * {@code null} indicates no minimum timestamp. */ public DateTime getUpdatedMin() { return this.updatedMin; } /** * Sets the maximum updated timestamp used for the query. Only entries with * an updated timestamp earlier than the specified timestamp will be returned. * * @param updatedMax maximum updated timestamp for matched entries. A value * of {@code null} disables maximum timestamp filtering. */ public void setUpdatedMax(DateTime updatedMax) { this.updatedMax = updatedMax; } /** * Returns the maximum updated timestamp used for this query. Only entries * with an updated timestamp earlier than the specified timestamp will be * returned. * * @return maximum updated timestamp for matched entries. A value of * {@code null} indicates no maximum timestamp. */ public DateTime getUpdatedMax() { return this.updatedMax; } /** * Sets the minimum published timestamp used for the query. Only entries with * a published time equal to or later than the specified timestamp will be * returned. * * @param publishedMin minimum published timestamp for matched entries. A * value of {@code null} disables minimum timestamp filtering. */ public void setPublishedMin(DateTime publishedMin) { this.publishedMin = publishedMin; } /** * Returns the minimum published timestamp used for this query. Only entries * with a published time equal to or later than the specified timestamp will * be returned. * * @return minimum published timestamp for matched entries. A value of * {@code null} indicates no minimum timestamp. */ public DateTime getPublishedMin() { return this.publishedMin; } /** * Sets the maximum published timestamp used for the query. Only entries with * a published time earlier than the specified timestamp will be returned. * * @param publishedMax maximum published timestamp for matched entries. A * value of {@code null} disables maximum timestamp filtering. */ public void setPublishedMax(DateTime publishedMax) { this.publishedMax = publishedMax; } /** * Returns the maximum published timestamp used for this query. Only entries * with a published timestamp earlier than the specified timestamp will be * returned. * * @return maximum published timestamp for matched entries. A value of * {@code null} indicates no maximum timestamp. */ public DateTime getPublishedMax() { return this.publishedMax; } /** * Sets the start index for query results. This is a 1-based index. * * @param startIndex the start index for query results. * @throws IllegalArgumentException if index is less than or equal to zero. */ public void setStartIndex(int startIndex) { if (startIndex != UNDEFINED && startIndex < 1) { throw new IllegalArgumentException("Start index must be positive"); } this.startIndex = startIndex; } /** * Returns the current start index value for the query, * or {@link #UNDEFINED} if start index has not been set. */ public int getStartIndex() { return startIndex; } /** * Sets the maximum number of results to return for the query. Note: * a GData server may choose to provide fewer results, but will * never provide more than the requested maximum. * * @param maxResults the maximum number of results to return for the query. * A value of zero indicates that the server is free * to determine the maximum value. * @throws IllegalArgumentException if the provided value is less than zero. */ public void setMaxResults(int maxResults) { if (maxResults != UNDEFINED && maxResults < 0) { throw new IllegalArgumentException("Max results must be zero or larger"); } this.maxResults = maxResults; } /** * Returns the maximum number of results to return for the query, * or {@link #UNDEFINED} if max results has not been set. * <p> * Note: a GData server may choose to provide fewer results, but will * never provide more than the requested maximum. */ public int getMaxResults() { return this.maxResults; } /** * Sets the expected query result format. * * @param resultFormat ResultFormat value indicating the desired format. */ public void setResultFormat(ResultFormat resultFormat) { this.resultFormat = resultFormat; } /** * Returns the query result format. * * @return ResultFormat associated with the query instance. */ public ResultFormat getResultFormat() { return resultFormat; } /** * Sets the strictness of parameter parsing. * * @param strict true if strict parsing should be enabled for this query. */ public void setStrict(boolean strict) { this.strict = strict; } /** * Returns the strictness setting for query parameter parsing on the server. * * @return true if strict parsing is enabled for this query. */ public boolean isStrict() { return strict; } /** * The CustomParameter class defines a base representation for custom query * parameters. */ public static class CustomParameter { private String name; private String value; /** * Constructs a new custom parameter with the specified name/value * pair. */ public CustomParameter(String name, String value) { this.name = name; this.value = value; } /** * Returns the name of the custom parameter. */ public String getName() { return name; } /** * Returns the value of the custom parameter. */ public String getValue() { return value; } } /** * Adds a new CustomParameter. * * @param customParameter the new custom parameter to add. */ public void addCustomParameter(CustomParameter customParameter) { if (customParameter == null) { throw new NullPointerException("Null custom parameter"); } customParameters.add(customParameter); } /** * Returns the list of custom parameters. * * @return all custom parameters for the query. An empty list will be * returned if there are no custom parameters. */ public List<CustomParameter> getCustomParameters() { return customParameters; } /** * Returns the list of custom parameters that match a specified name. * * @param name the name value to match for returned parameters. * @return all parameters that have the specified name. An empty list will * be returned if there are no matching parameters. */ public List<CustomParameter> getCustomParameters(String name) { List<CustomParameter> matchList = new ArrayList<CustomParameter>(); for (CustomParameter param : customParameters) { if (param.name.equals(name)) { matchList.add(param); } } return matchList; } /** * Appends specified query (parameter, value) to provided query URL buffer. * * @param queryBuf base URI buffer to append to. * @param paramName query parameter name. * @param paramValue query parameter value. */ protected void appendQueryParameter(StringBuilder queryBuf, String paramName, String paramValue) throws UnsupportedEncodingException { queryBuf.append(queryBuf.length() != 0 ? '&' : '?'); queryBuf.append(paramName); queryBuf.append("="); queryBuf.append(paramValue); } /** * Check if current query state is supported. * * @return <code>true</code> if supported. */ public boolean isValidState() { // Check if requested ResultFormat is supported return (resultFormat != ResultFormat.JSON_XD); } /** * Returns the relative query URI that represents only the query * parameters without any components related to the target feed. * Subclasses of the Query class may override this method to add * additional URI path elements or HTTP query parameters to represent * service-specific parameters. * * @return URI representing current query. */ public URI getQueryUri() { if (!isValidState()) { throw new IllegalStateException("Unsupported Query"); } StringBuilder pathBuf = new StringBuilder(); try { if (categoryFilters.size() != 0) { pathBuf.append("-"); // signals beginning of query path elements for (CategoryFilter categoryFilter : categoryFilters) { pathBuf.append("/"); pathBuf.append( CharEscapers.uriEscaper().escape(categoryFilter.toString())); } } StringBuilder queryBuf = new StringBuilder(); if (queryString != null) { appendQueryParameter(queryBuf, GDataProtocol.Query.FULL_TEXT, CharEscapers.uriEscaper().escape(queryString)); } if (author != null) { appendQueryParameter(queryBuf, GDataProtocol.Query.AUTHOR, CharEscapers.uriEscaper().escape(author)); } if (resultFormat != ResultFormat.DEFAULT) { appendQueryParameter(queryBuf, GDataProtocol.Parameter.ALT, resultFormat.paramValue()); } if (updatedMin != null) { appendQueryParameter(queryBuf, GDataProtocol.Query.UPDATED_MIN, CharEscapers.uriEscaper().escape(updatedMin.toString())); } if (updatedMax != null) { appendQueryParameter(queryBuf, GDataProtocol.Query.UPDATED_MAX, CharEscapers.uriEscaper().escape(updatedMax.toString())); } if (publishedMin != null) { appendQueryParameter(queryBuf, GDataProtocol.Query.PUBLISHED_MIN, CharEscapers.uriEscaper().escape(publishedMin.toString())); } if (publishedMax != null) { appendQueryParameter(queryBuf, GDataProtocol.Query.PUBLISHED_MAX, CharEscapers.uriEscaper().escape(publishedMax.toString())); } if (startIndex != UNDEFINED) { appendQueryParameter(queryBuf, GDataProtocol.Query.START_INDEX, Integer.toString(startIndex)); } if (maxResults != UNDEFINED) { appendQueryParameter(queryBuf, GDataProtocol.Query.MAX_RESULTS, Integer.toString(maxResults)); } if (fields != null) { appendQueryParameter(queryBuf, GDataProtocol.Query.FIELDS, CharEscapers.uriEscaper().escape(fields)); } if (strict) { appendQueryParameter(queryBuf, GDataProtocol.Parameter.STRICT, "true"); } for (CustomParameter customParameter : customParameters) { appendQueryParameter(queryBuf, CharEscapers.uriEscaper().escape(customParameter.name), CharEscapers.uriEscaper().escape(customParameter.value)); } return new URI(pathBuf.toString() + queryBuf.toString()); } catch (UnsupportedEncodingException uee) { throw new IllegalStateException("Unable to encode query URI", uee); } catch (URISyntaxException use) { // This would indicate a programming error above, not user error throw new IllegalStateException("Unable to construct query URI", use); } } /** * Returns the Query URL that encapsulates the current state of this * query object. * * @return URL that represents the query against the target feed. */ public URL getUrl() { try { String queryUri = getQueryUri().toString(); if (queryUri.length() == 0) { return feedUrl; } // Build the full query URL. An earlier implementation of this // was done using URI.resolve(), but there are issues if both the // base and relative URIs contain path components (the last path // element on the base will be removed). String feedRoot = feedUrl.toString(); StringBuilder urlBuf = new StringBuilder(feedRoot); if (!feedRoot.endsWith("/") && !queryUri.startsWith("?")) { urlBuf.append('/'); } urlBuf.append(queryUri); return new URL(urlBuf.toString()); // Since we are combining a valid URL and a valid URI, // any exception thrown below is not a user error. } catch (MalformedURLException mue) { throw new IllegalStateException("Unable to create query URL", mue); } } /** * Sets a string custom parameter, with null signifying to clear the * parameter. * * @param name the name of the parameter * @param value the value to set it to */ public final void setStringCustomParameter(String name, String value) { List<CustomParameter> customParams = getCustomParameters(); for (CustomParameter existingValue : getCustomParameters(name)) { customParams.remove(existingValue); } if (value != null) { customParams.add(new CustomParameter(name, value)); } } /** * Gets an existing String custom parameter, with null signifying that the * parameter is not specified. * * @param name the name of the parameter * @return the value, or null if there is no parameter */ public final String getStringCustomParameter(String name) { List<CustomParameter> params = getCustomParameters(name); if (params.size() == 0) { return null; } else { return params.get(0).getValue(); } } /** * Sets an integer custom paramter, with null signifying to clear the * parameter. * * @param name the parameter name * @param value the value to set it to */ public final void setIntegerCustomParameter(String name, Integer value) { if (value == null) { setStringCustomParameter(name, null); } else { setStringCustomParameter(name, value.toString()); } } /** * Gets an existing Integer custom paramter, with null signifying that * the parameter is not specified or not an integer. * * @param name the name of the parameter * @return the value of the parameter, or null if it is unspecified * or non-integer */ public final Integer getIntegerCustomParameter(String name) { String strValue = getStringCustomParameter(name); Integer intValue; if (strValue != null) { try { intValue = Integer.valueOf(Integer.parseInt(strValue)); } catch (NumberFormatException nfe) { intValue = null; } } else { intValue = null; } return intValue; } }