001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
002: * This code is licensed under the GPL 2.0 license, availible at the root
003: * application directory.
004: */
005: package org.geoserver.ows.kvp;
006:
007: import java.text.DateFormat;
008: import java.text.ParseException;
009: import java.text.ParsePosition;
010: import java.text.SimpleDateFormat;
011: import java.util.ArrayList;
012: import java.util.Collections;
013: import java.util.Date;
014: import java.util.List;
015: import java.util.Locale;
016: import org.geoserver.ows.KvpParser;
017:
018: /**
019: * Parses the {@code time} parameter of the request. The date, time and period
020: * are expected to be formatted according ISO-8601 standard.
021: *
022: * @author Cédric Briancon
023: * @author Martin Desruisseaux
024: * @version $Id: TimeKvpParser.java 7648 2007-10-23 15:25:38Z aaime $
025: */
026: public class TimeKvpParser extends KvpParser {
027: /**
028: * All patterns that are correct regarding the ISO-8601 norm.
029: */
030: private static final String[] PATTERNS = {
031: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd'T'HH:mm:ss'Z'",
032: "yyyy-MM-dd'T'HH:mm'Z'", "yyyy-MM-dd'T'HH'Z'",
033: "yyyy-MM-dd", "yyyy-MM", "yyyy" };
034:
035: /**
036: * Amount of milliseconds in a day.
037: */
038: static final long MILLIS_IN_DAY = 24 * 60 * 60 * 1000;
039:
040: /**
041: * Date formats to be used in order to parse the String given by the user in the request.
042: */
043: private final DateFormat[] formats = new DateFormat[PATTERNS.length];
044:
045: /**
046: * Creates the parser specifying the name of the key to latch to.
047: *
048: * @param key The key whose associated value to parse.
049: */
050: public TimeKvpParser(String key) {
051: super (key, List.class);
052: }
053:
054: /**
055: * Parses the date given in parameter. The date format should comply to
056: * ISO-8601 standard. The string may contains either a single date, or
057: * a start time, end time and a period. In the first case, this method
058: * returns a singleton containing only the parsed date. In the second
059: * case, this method returns a list including all dates from start time
060: * up to the end time with the interval specified in the {@code value}
061: * string.
062: *
063: * @param value The date, time and period to parse.
064: * @return A list of dates, or an empty list of the {@code value} string
065: * is null or empty.
066: * @throws ParseException if the string can not be parsed.
067: */
068: public Object parse(String value) throws ParseException {
069: if (value == null) {
070: return Collections.EMPTY_LIST;
071: }
072: value = value.trim();
073: if (value.length() == 0) {
074: return Collections.EMPTY_LIST;
075: }
076: final List dates = new ArrayList();
077: if (value.indexOf(',') >= 0) {
078: String[] listDates = value.split(",");
079: for (int i = 0; i < listDates.length; i++) {
080: dates.add(getDate(listDates[i].trim()));
081: }
082: return dates;
083: }
084: String[] period = value.split("/");
085: // Only one date given.
086: if (period.length == 1) {
087: dates.add(getDate(value));
088: return dates;
089: }
090: // Period like : yyyy-MM-ddTHH:mm:ssZ/yyyy-MM-ddTHH:mm:ssZ/P1D
091: if (period.length == 3) {
092: final Date begin = getDate(period[0]);
093: final Date end = getDate(period[1]);
094: final long millisIncrement = parsePeriod(period[2]);
095: final long startTime = begin.getTime();
096: final long endTime = end.getTime();
097: long time;
098: int j = 0;
099: while ((time = j * millisIncrement + startTime) <= endTime) {
100: dates.add(new Date(time));
101: j++;
102: }
103: return dates;
104: }
105: throw new ParseException("Invalid time parameter: " + value, 0);
106: }
107:
108: /**
109: * Parses date given in parameter according the ISO-8601 standard. This parameter
110: * should follow a syntax defined in the {@link #PATTERNS} array to be validated.
111: *
112: * @param value The date to parse.
113: * @return A date found in the request.
114: * @throws ParseException if the string can not be parsed.
115: */
116: private Date getDate(final String value) throws ParseException {
117: for (int i = 0; i < formats.length; i++) {
118: if (formats[i] == null) {
119: formats[i] = new SimpleDateFormat(PATTERNS[i],
120: Locale.CANADA);
121: }
122: /* We do not use the standard method DateFormat.parse(String), because if the parsing
123: * stops before the end of the string, the remaining characters are just ignored and
124: * no exception is thrown. So we have to ensure that the whole string is correct for
125: * the format.
126: */
127: ParsePosition pos = new ParsePosition(0);
128: Date time = formats[i].parse(value, pos);
129: if (pos.getIndex() == value.length()) {
130: return time;
131: }
132: }
133: throw new ParseException("Invalid date: " + value, 0);
134: }
135:
136: /**
137: * Parses the increment part of a period and returns it in milliseconds.
138: *
139: * @param period A string representation of the time increment according the ISO-8601:1988(E)
140: * standard. For example: {@code "P1D"} = one day.
141: * @return The increment value converted in milliseconds.
142: * @throws ParseException if the string can not be parsed.
143: */
144: static long parsePeriod(final String period) throws ParseException {
145: final int length = period.length();
146: if (length != 0
147: && Character.toUpperCase(period.charAt(0)) != 'P') {
148: throw new ParseException("Invalid period increment given: "
149: + period, 0);
150: }
151: long millis = 0;
152: boolean time = false;
153: int lower = 0;
154: while (++lower < length) {
155: char letter = Character.toUpperCase(period.charAt(lower));
156: if (letter == 'T') {
157: time = true;
158: if (++lower >= length) {
159: break;
160: }
161: }
162: int upper = lower;
163: letter = period.charAt(upper);
164: while (!Character.isLetter(letter) || letter == 'e'
165: || letter == 'E') {
166: if (++upper >= length) {
167: throw new ParseException("Missing symbol in \""
168: + period + "\".", lower);
169: }
170: letter = period.charAt(upper);
171: }
172: letter = Character.toUpperCase(letter);
173: final double value = Double.parseDouble(period.substring(
174: lower, upper));
175: final double factor;
176: if (time) {
177: switch (letter) {
178: case 'S':
179: factor = 1000;
180: break;
181: case 'M':
182: factor = 60 * 1000;
183: break;
184: case 'H':
185: factor = 60 * 60 * 1000;
186: break;
187: default:
188: throw new ParseException("Unknown time symbol: "
189: + letter, upper);
190: }
191: } else {
192: switch (letter) {
193: case 'D':
194: factor = MILLIS_IN_DAY;
195: break;
196: case 'W':
197: factor = 7 * MILLIS_IN_DAY;
198: break;
199: // TODO: handle months in a better way than just taking the average length.
200: case 'M':
201: factor = 30 * MILLIS_IN_DAY;
202: break;
203: case 'Y':
204: factor = 365.25 * MILLIS_IN_DAY;
205: break;
206: default:
207: throw new ParseException("Unknown period symbol: "
208: + letter, upper);
209: }
210: }
211: millis += Math.round(value * factor);
212: lower = upper;
213: }
214: return millis;
215: }
216: }
|