/* 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.calendar;
import com.google.gdata.client.Query;
import com.google.gdata.data.DateTime;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The CalendarQuery class extends the base GData Query class to
* define convenience APIs for Calendar custom query parameters.
*
*
*
*/
public class CalendarQuery extends Query {
/**
* The name of the custom query parameter that specifies that all
* events returned must be greater than or equal to the specified
* time.
*/
public static final String MINIMUM_START_TIME = "start-min";
/**
* The name of the custom query parameter that specifies that all
* events returned must be less than the specified time.
*/
public static final String MAXIMUM_START_TIME = "start-max";
/**
* The name of the extended property query parameter that specifies that all
* events' extended properties must have matching values.
*/
public static final String EXT_PROP_QUERY = "extq";
/**
* An empty array of ExtendedPropertyMatch which will be returned
* from {@link #getExtendedPropertyQuery()} should the current
* extended property query be empty.
*/
private static final ExtendedPropertyMatch[] EMPTY_EXT_PROP_MATCH =
new ExtendedPropertyMatch[0];
/**
* The ExtendedPropertyMatch class corresponds to a single
* extended property match.
*
* @see CalendarQuery#setExtendedPropertyQuery(
* CalendarQuery.ExtendedPropertyMatch...)
* @see CalendarQuery#getExtendedPropertyQuery()
*/
public static class ExtendedPropertyMatch {
/** The maximum length of an extended property name. */
public static final int MAX_EXTENDED_PROPERTY_NAME_LENGTH = 44;
/**
* The maximum length of an extended property value (after escaped
* characters have been converted into plain characters, e.g. \" -> ").
*/
public static final int MAX_EXTENDED_PROPERTY_VALUE_LENGTH = 1024;
/**
* A regex describing the format of extended property names.
* The name can be a plain string without :'s and ='s.
*/
public static final String PROPERTY_NAME_REGEX = "[^:=]+";
/**
* A regex describing the format of extended property values.
* The value can be:
* <ul>
* <li>a (possibly empty) plain string without ]'s (e.g. Foo Property), or
* <li>a (possibly empty) quoted string with back slashes and quotes
* escaped by back slashes (e.g. "\"[Property name]\"").
* </ul>
*/
public static final String PROPERTY_VALUE_REGEX =
"\\\"(?:[^\\\"\\\\]|(?:\\\\\\\\)|(?:\\\\\\\"))*\\\"|[^\\]]*";
/**
* Group 1: property name (without :'s and ='s).
* Group 2: property value (perhaps with surrounding quotes which should be
* stripped off).
*/
public static final String SINGLE_EXT_PROP_QUERY_REGEX =
"\\[(" + PROPERTY_NAME_REGEX + "):" +
"(" + PROPERTY_VALUE_REGEX + ")\\]";
/**
* A pattern that matches exactly one extended property query within
* a compound extended property query. E.g. when applied to
* {@code "[foo:bar][baz:"bin"]"}, it will consecutively match:
* <ol>
* <li>[foo:bar] (group1: foo, group2: bar).
* <li>[baz:"bin"] (group1: baz, group2: "bin". Please note that the
* quotes surrounding 'bin' also belong in the group text and
* <em>should be stripped</em> prior to further processing
* of the property value).
* </ol>
*/
public static final Pattern EXT_PROP_QUERY_PATTERN = Pattern.compile(
SINGLE_EXT_PROP_QUERY_REGEX);
private String name;
private String expr;
/**
* @param name extended property name. May contain up to 44 characters and
* may not contain ':' or '=' characters.
* @param value to match against the {@code name} extended
* property. May contain up to 1024 characters.
*/
public ExtendedPropertyMatch(String name, String value) {
if (name == null) {
throw new NullPointerException("Property name is null");
}
if (value == null) {
throw new NullPointerException("Property value is null");
}
if (name.length() > MAX_EXTENDED_PROPERTY_NAME_LENGTH) {
throw new IllegalArgumentException(
"Property name length in characters must not be more than " +
MAX_EXTENDED_PROPERTY_NAME_LENGTH);
}
if (value.length() > MAX_EXTENDED_PROPERTY_VALUE_LENGTH) {
throw new IllegalArgumentException(
"Property value length in characters must not be more than " +
MAX_EXTENDED_PROPERTY_VALUE_LENGTH);
}
this.name = name;
this.expr = value;
}
public String getName() {
return name;
}
public String getExpression() {
return expr;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('[').append(name).append(':');
appendExpr(sb);
sb.append(']');
return sb.toString();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof ExtendedPropertyMatch)) {
return false;
}
ExtendedPropertyMatch that = (ExtendedPropertyMatch) other;
return safeEquals(this.name, that.name) &&
safeEquals(this.expr, that.expr);
}
@Override
public int hashCode() {
return name.hashCode() * 0x101 +
expr.hashCode() * 0x11;
}
private static boolean safeEquals(Object left, Object right) {
return left == right || (left != null && left.equals(right));
}
private void appendExpr(StringBuilder sb) {
if (expr.contains("]") || expr.contains("\"")) {
sb.append('\"').
append(expr.replaceAll("\\\\", "\\\\\\\\").
replaceAll("\\\"", "\\\\\"")).
append('\"');
} else {
sb.append(expr);
}
}
/**
* Converts strings of the form "[foo:bar][baz:bin]" to a map of
* "foo"->"bar" and "baz"->"bin".
* Ignores quotation marks around values but not around keys.
* Unescapes backslash-escaped characters.
*
* @param extqQuery a non-null query string.
* @return array of {@link ExtendedPropertyMatch}.
* @throws IllegalArgumentException whenever the extended property query
* syntax passed in is invalid.
*/
public static ExtendedPropertyMatch[]
arrayFromExtendedPropertyQueryString(String extqQuery) {
List<ExtendedPropertyMatch> result =
new LinkedList<ExtendedPropertyMatch>();
int startPos = 0;
Matcher m = EXT_PROP_QUERY_PATTERN.matcher(extqQuery);
while (startPos < extqQuery.length()) {
if (!m.find(startPos) || m.start() != startPos) {
throw new IllegalArgumentException(
"Invalid extended property query:" + extqQuery);
}
startPos = m.end();
String propName = m.group(1);
String propValue = m.group(2);
if (propValue.startsWith("\"") && propValue.endsWith("\"")) {
propValue = propValue.substring(1, propValue.length() - 1).
replaceAll("\\\\(.)", "$1");
}
result.add(new ExtendedPropertyMatch(propName, propValue));
}
return result.toArray(EMPTY_EXT_PROP_MATCH);
}
}
/**
* Constructs a new CalendarQuery 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 CalendarQuery(URL feedUrl) {
super(feedUrl);
}
/**
* Sets the minimum start time for events returned by the query. Together
* with {@link #setMaximumStartTime} creates a timespan such that only events
* that are within the timespan are returned.
*
* @param minStart the minimum start time. A value of {@code null}
* disables querying by minimum start time.
*/
public void setMinimumStartTime(DateTime minStart) {
List<CustomParameter> customParams = getCustomParameters();
// Remove any existing minimum start value.
for (CustomParameter existingValue :
getCustomParameters(MINIMUM_START_TIME)) {
customParams.remove(existingValue);
}
// Add the specified value.
if (minStart != null) {
customParams.add(new CustomParameter(MINIMUM_START_TIME,
minStart.toString()));
}
}
/**
* Returns the minimum start time for events returned by the query. Only
* events that start on or after this time will be returned.
*
* @return the minimum start time. A value of {@code null} indicates
* that minimum start-time based querying is disabled.
*/
public DateTime getMinimumStartTime() {
List<CustomParameter> minParams = getCustomParameters(MINIMUM_START_TIME);
if (minParams.size() == 0)
return null;
return DateTime.parseDateTime(minParams.get(0).getValue());
}
/**
* Sets the maximum start time for events returned by the query. Together
* with {@link #setMinimumStartTime} creates a timespan such that only events
* that are within the timespan are returned.
*
* @param maxStart the maximum start time. A value of {@code null}
* disables querying by maximum start time.
*/
public void setMaximumStartTime(DateTime maxStart) {
List<CustomParameter> customParams = getCustomParameters();
// Remove any existing maximum start value.
for (CustomParameter existingValue :
getCustomParameters(MAXIMUM_START_TIME)) {
customParams.remove(existingValue);
}
// Add the specified value.
if (maxStart != null) {
customParams.add(new CustomParameter(MAXIMUM_START_TIME,
maxStart.toString()));
}
}
/**
* Returns the maximum start time for events returned by the query. Only
* events that start before this time will be returned.
*
* @return the maximum start time. A value of {@code null} indicates
* that maximum start-time based querying is disabled.
*/
public DateTime getMaximumStartTime() {
List<CustomParameter> maxParams = getCustomParameters(MAXIMUM_START_TIME);
if (maxParams.size() == 0)
return null;
return DateTime.parseDateTime(maxParams.get(0).getValue());
}
/**
* Sets up the extended property matching for events returned by the query
* by setting the {@code extq} custom parameter value.
*
* @param matches extended property matches.
* Only events that satisfy all of these will be returned.
* A value of {@code null} or an empty array of matches disables
* extended property matching for this CalendarQuery.
*/
public void setExtendedPropertyQuery(ExtendedPropertyMatch... matches) {
if (matches == null || matches.length == 0) {
setStringCustomParameter(EXT_PROP_QUERY, null);
return;
}
StringBuilder query = new StringBuilder();
for (ExtendedPropertyMatch m : matches) {
query.append(m.toString());
}
setStringCustomParameter(EXT_PROP_QUERY, query.toString());
}
/**
* Returns an array of extended property matches parsed from
* the current value of {@code extq} custom parameter.
*
* @return the extended property query text. An empty array
* shall be returned when extended property matching
* is disabled for this CalendarQuery.
*/
public ExtendedPropertyMatch[] getExtendedPropertyQuery() {
String query = getStringCustomParameter(EXT_PROP_QUERY);
if (query == null) {
return EMPTY_EXT_PROP_MATCH;
}
return ExtendedPropertyMatch.arrayFromExtendedPropertyQueryString(query);
}
}