1 /*
2 * Copyright (C) 2007 ETH Zurich
3 *
4 * This file is part of Fosstrak (www.fosstrak.org).
5 *
6 * Fosstrak is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License version 2.1, as published by the Free Software Foundation.
9 *
10 * Fosstrak is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with Fosstrak; if not, write to the Free
17 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301 USA
19 */
20
21 package org.fosstrak.epcis.utils;
22
23 import java.sql.Timestamp;
24 import java.text.DecimalFormat;
25 import java.text.ParseException;
26 import java.util.Calendar;
27 import java.util.Date;
28 import java.util.GregorianCalendar;
29 import java.util.TimeZone;
30
31 /**
32 * The <code>TimeParser</code> utility class provides helper methods to deal
33 * with date/time formatting using a specific ISO8601-compliant format (see <a
34 * href="http://www.w3.org/TR/NOTE-datetime">ISO 8601</a>). <p/> The currently
35 * supported format is:
36 *
37 * <pre>
38 * &plusmnYYYY-MM-DDThh:mm:ss[.S]TZD
39 * </pre>
40 *
41 * where:
42 *
43 * <pre>
44 * &plusmnYYYY = four-digit year with optional sign where values <= 0 are
45 * denoting years BCE and values > 0 are denoting years CE,
46 * e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE,
47 * 0001 denotes the year 1 CE, and so on...
48 * MM = two-digit month (01=January, etc.)
49 * DD = two-digit day of month (01 through 31)
50 * hh = two digits of hour (00 through 23) (am/pm NOT allowed)
51 * mm = two digits of minute (00 through 59)
52 * ss = two digits of second (00 through 59)
53 * S = optional one or more digits representing a decimal fraction of a second
54 * TZD = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC
55 * in the form of +hh:mm or -hh:mm
56 * </pre>
57 */
58 public final class TimeParser {
59
60 /**
61 * Miscellaneous numeric formats used in formatting.
62 */
63 private static final DecimalFormat XX_FORMAT = new DecimalFormat("00");
64 private static final DecimalFormat XXX_FORMAT = new DecimalFormat("000");
65 private static final DecimalFormat XXXX_FORMAT = new DecimalFormat("0000");
66
67 /**
68 * Empty private constructor to hide default constructor.
69 */
70 private TimeParser() {
71 }
72
73 /**
74 * Parses an ISO8601-compliant date/time string into a <code>Calendar</code>.
75 *
76 * @param text
77 * The date/time string to be parsed.
78 * @return A <code>Calendar</code> representing the date/time.
79 * @throws ParseException
80 * If the date/time could not be parsed.
81 */
82 public static Calendar parseAsCalendar(final String text) throws ParseException {
83 return parse(text);
84 }
85
86 /**
87 * Parses an ISO8601-compliant date/time string into a <code>Date</code>.
88 *
89 * @param text
90 * The date/time string to be parsed.
91 * @return A <code>Date</code> representing the date/time.
92 * @throws ParseException
93 * If the date/time could not be parsed.
94 */
95 public static Date parseAsDate(final String text) throws ParseException {
96 return parse(text).getTime();
97 }
98
99 /**
100 * Parses an ISO8601-compliant date/time string into a
101 * <code>Timestamp</code>.
102 *
103 * @param text
104 * The date/time string to be parsed.
105 * @return A <code>Timestamp</code> representing the date/time.
106 * @throws ParseException
107 * If the date/time could not be parsed.
108 */
109 public static Timestamp parseAsTimestamp(final String text) throws ParseException {
110 return convert(parse(text));
111 }
112
113 /**
114 * Parses an ISO8601-compliant date/time string into a <code>Calendar</code>.
115 *
116 * @param text
117 * The date/time string to be parsed.
118 * @return A <code>Calendar</code> representing the date/time.
119 * @throws ParseException
120 * If the date/time could not be parsed.
121 */
122 private static Calendar parse(final String text) throws ParseException {
123 try {
124 String time = text;
125 if (time == null || time.length() == 0) {
126 throw new IllegalArgumentException("Date/Time string may not be null or empty.");
127 }
128 time = time.trim();
129 char sign;
130 int curPos;
131 if (time.startsWith("-")) {
132 sign = '-';
133 curPos = 1;
134 } else if (time.startsWith("+")) {
135 sign = '+';
136 curPos = 1;
137 } else {
138 sign = '+'; // no sign specified, implied '+'
139 curPos = 0;
140 }
141
142 int year, month, day, hour, min, sec, ms;
143 String tzID;
144 char delimiter;
145
146 // parse year
147 try {
148 year = Integer.parseInt(time.substring(curPos, curPos + 4));
149 } catch (NumberFormatException e) {
150 throw new ParseException("Year (YYYY) has wrong format: " + e.getMessage(), curPos);
151 }
152 curPos += 4;
153 delimiter = '-';
154 if (curPos >= time.length() || time.charAt(curPos) != delimiter) {
155 throw new ParseException("expected delimiter '" + delimiter + "' at position " + curPos, curPos);
156 }
157 curPos++;
158
159 // parse month
160 try {
161 month = Integer.parseInt(time.substring(curPos, curPos + 2));
162 } catch (NumberFormatException e) {
163 throw new ParseException("Month (MM) has wrong format: " + e.getMessage(), curPos);
164 }
165 curPos += 2;
166 delimiter = '-';
167 if (curPos >= time.length() || time.charAt(curPos) != delimiter) {
168 throw new ParseException("expected delimiter '" + delimiter + "' at position " + curPos, curPos);
169 }
170 curPos++;
171
172 // parse day
173 try {
174 day = Integer.parseInt(time.substring(curPos, curPos + 2));
175 } catch (NumberFormatException e) {
176 throw new ParseException("Day (DD) has wrong format: " + e.getMessage(), curPos);
177 }
178 curPos += 2;
179 delimiter = 'T';
180 if (curPos >= time.length() || time.charAt(curPos) != delimiter) {
181 throw new ParseException("expected delimiter '" + delimiter + "' at position " + curPos, curPos);
182 }
183 curPos++;
184
185 // parse hours
186 try {
187 hour = Integer.parseInt(time.substring(curPos, curPos + 2));
188 } catch (NumberFormatException e) {
189 throw new ParseException("Hour (hh) has wrong format: " + e.getMessage(), curPos);
190 }
191 curPos += 2;
192 delimiter = ':';
193 if (curPos >= time.length() || time.charAt(curPos) != delimiter) {
194 throw new ParseException("expected delimiter '" + delimiter + "' at position " + curPos, curPos);
195 }
196 curPos++;
197
198 // parse minute
199 try {
200 min = Integer.parseInt(time.substring(curPos, curPos + 2));
201 } catch (NumberFormatException e) {
202 throw new ParseException("Minute (mm) has wrong format: " + e.getMessage(), curPos);
203 }
204 curPos += 2;
205 delimiter = ':';
206 if (curPos >= time.length() || time.charAt(curPos) != delimiter) {
207 throw new ParseException("expected delimiter '" + delimiter + "' at position " + curPos, curPos);
208 }
209 curPos++;
210
211 // parse second
212 try {
213 sec = Integer.parseInt(time.substring(curPos, curPos + 2));
214 } catch (NumberFormatException e) {
215 throw new ParseException("Second (ss) has wrong format: " + e.getMessage(), curPos);
216 }
217 curPos += 2;
218
219 // parse millisecond
220 delimiter = '.';
221 if (curPos < time.length() && time.charAt(curPos) == delimiter) {
222 curPos++;
223 try {
224 // read all digits (number of digits unknown)
225 StringBuilder millis = new StringBuilder();
226 while (curPos < time.length() && isNumeric(time.charAt(curPos))) {
227 millis.append(time.charAt(curPos));
228 curPos++;
229 }
230 // convert to milliseconds (max 3 digits)
231 if (millis.length() == 1) {
232 ms = 100 * Integer.parseInt(millis.toString());
233 } else if (millis.length() == 2) {
234 ms = 10 * Integer.parseInt(millis.toString());
235 } else if (millis.length() >= 3) {
236 ms = Integer.parseInt(millis.substring(0, 3));
237 if (millis.length() > 3) {
238 // round
239 if (Integer.parseInt(String.valueOf(millis.charAt(3))) >= 5) {
240 ms++;
241 }
242 }
243 } else {
244 ms = 0;
245 }
246 } catch (NumberFormatException e) {
247 throw new ParseException("Millisecond (S) has wrong format: " + e.getMessage(), curPos);
248 }
249 } else {
250 ms = 0;
251 }
252
253 // parse time zone designator (Z or +00:00 or -00:00)
254 if (curPos < time.length() && (time.charAt(curPos) == '+' || time.charAt(curPos) == '-')) {
255 // offset to UTC specified in the format +00:00/-00:00
256 tzID = "GMT" + time.substring(curPos);
257 } else if (curPos < time.length() && time.substring(curPos).equals("Z")) {
258 tzID = "UTC";
259 } else {
260 // throw new ParseException("invalid time zone designator",
261 // curPos);
262 // no time zone designator found, using default 'UTC'
263 tzID = "UTC";
264 }
265
266 TimeZone tz = TimeZone.getTimeZone(tzID);
267 // verify id of returned time zone (getTimeZone defaults to "UTC")
268 if (!tz.getID().equals(tzID)) {
269 throw new ParseException("invalid time zone '" + tzID + "'", curPos);
270 }
271
272 // initialize Calendar object
273 Calendar cal = GregorianCalendar.getInstance(tz);
274 cal.setLenient(false);
275 if (sign == '-' || year == 0) {
276 // not CE, need to set era (BCE) and adjust year
277 cal.set(Calendar.YEAR, year + 1);
278 cal.set(Calendar.ERA, GregorianCalendar.BC);
279 } else {
280 cal.set(Calendar.YEAR, year);
281 cal.set(Calendar.ERA, GregorianCalendar.AD);
282 }
283 cal.set(Calendar.MONTH, month - 1); // month is 0-based
284 cal.set(Calendar.DAY_OF_MONTH, day);
285 cal.set(Calendar.HOUR_OF_DAY, hour);
286 cal.set(Calendar.MINUTE, min);
287 cal.set(Calendar.SECOND, sec);
288 cal.set(Calendar.MILLISECOND, ms);
289
290 // the following will trigger an IllegalArgumentException if any of
291 // the set values are illegal or out of range
292 cal.getTime();
293
294 return cal;
295 } catch (StringIndexOutOfBoundsException e) {
296 throw new ParseException("date/time value has invalid format", -1);
297 }
298 }
299
300 /**
301 * Formats a <code>Date</code> value into an ISO8601-compliant date/time
302 * string.
303 *
304 * @param date
305 * The time value to be formatted into a date/time string.
306 * @return The formatted date/time string.
307 */
308 public static String format(final Date date) {
309 Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
310 cal.setTimeInMillis(date.getTime());
311 return format(cal);
312 }
313
314 /**
315 * Formats a <code>Timestamp</code> value into an ISO8601-compliant
316 * date/time string.
317 *
318 * @param ts
319 * The time value to be formatted into a date/time string.
320 * @return The formatted date/time string.
321 */
322 public static String format(final Timestamp ts) {
323 return format(convert(ts));
324 }
325
326 /**
327 * Formats a <code>Calendar</code> value into an ISO8601-compliant
328 * date/time string.
329 *
330 * @param cal
331 * The time value to be formatted into a date/time string.
332 * @return The formatted date/time string.
333 */
334 public static String format(final Calendar cal) {
335 if (cal == null) {
336 throw new IllegalArgumentException("argument can not be null");
337 }
338
339 // determine era and adjust year if necessary
340 int year = cal.get(Calendar.YEAR);
341 if (cal.isSet(Calendar.ERA) && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
342 /**
343 * calculate year using astronomical system: year n BCE =>
344 * astronomical year -n + 1
345 */
346 year = 0 - year + 1;
347 }
348
349 /**
350 * the format of the date/time string is: YYYY-MM-DDThh:mm:ss.SSSTZD
351 * note that we cannot use java.text.SimpleDateFormat for formatting
352 * because it can't handle years <= 0 and TZD's
353 */
354 StringBuilder buf = new StringBuilder();
355 // year ([-]YYYY)
356 buf.append(XXXX_FORMAT.format(year));
357 buf.append('-');
358 // month (MM)
359 buf.append(XX_FORMAT.format(cal.get(Calendar.MONTH) + 1));
360 buf.append('-');
361 // day (DD)
362 buf.append(XX_FORMAT.format(cal.get(Calendar.DAY_OF_MONTH)));
363 buf.append('T');
364 // hour (hh)
365 buf.append(XX_FORMAT.format(cal.get(Calendar.HOUR_OF_DAY)));
366 buf.append(':');
367 // minute (mm)
368 buf.append(XX_FORMAT.format(cal.get(Calendar.MINUTE)));
369 buf.append(':');
370 // second (ss)
371 buf.append(XX_FORMAT.format(cal.get(Calendar.SECOND)));
372 buf.append('.');
373 // millisecond (SSS)
374 buf.append(XXX_FORMAT.format(cal.get(Calendar.MILLISECOND)));
375 // time zone designator (Z or +00:00 or -00:00)
376 TimeZone tz = cal.getTimeZone();
377 // determine offset of timezone from UTC (incl. daylight saving)
378 int offset = tz.getOffset(cal.getTimeInMillis());
379 if (offset != 0) {
380 int hours = Math.abs((offset / (60 * 1000)) / 60);
381 int minutes = Math.abs((offset / (60 * 1000)) % 60);
382 buf.append(offset < 0 ? '-' : '+');
383 buf.append(XX_FORMAT.format(hours));
384 buf.append(':');
385 buf.append(XX_FORMAT.format(minutes));
386 } else {
387 buf.append('Z');
388 }
389 return buf.toString();
390 }
391
392 /**
393 * Checks whether the given character is Numeric.
394 *
395 * @param c
396 * The character to check.
397 * @return <code>true</code> if the given character is numeric,
398 * <code>false</code> otherwise.
399 */
400 private static boolean isNumeric(final char c) {
401 return (((c >= '0') && (c <= '9')) ? true : false);
402 }
403
404 /**
405 * Converts an SQL timestamp value into a Calendar object.
406 *
407 * @param ts
408 * The java.sql.Timestamp to convert.
409 * @return The Calendar object representing the given timestamp.
410 */
411 public static Calendar convert(final Timestamp ts) {
412 Calendar cal = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"));
413 cal.setTimeInMillis(ts.getTime());
414 return cal;
415 }
416
417 /**
418 * Converts a Calendar object into an SQL Timestamp value.
419 *
420 * @param cal
421 * The Calendar object to convert.
422 * @return The java.sql.Timestamp representing the given Calendar value.
423 */
424 public static Timestamp convert(final Calendar cal) {
425 return new Timestamp(cal.getTimeInMillis());
426 }
427
428 }