View Javadoc

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   *             &amp;plusmnYYYY-MM-DDThh:mm:ss[.S]TZD
39   * </pre>
40   * 
41   * where:
42   * 
43   * <pre>
44   *             &amp;plusmnYYYY = four-digit year with optional sign where values &lt;= 0 are
45   *                     denoting years BCE and values &gt; 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 }