This project has retired. For details please refer to its Attic page.
ISO8601 xref

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.chemistry.opencmis.jcr.util;
18  
19  import java.util.Calendar;
20  import java.util.GregorianCalendar;
21  import java.util.TimeZone;
22  
23  /**
24   * The <code>ISO8601</code> utility class provides helper methods
25   * to deal with date/time formatting using a specific ISO8601-compliant
26   * format (see <a href="http://www.w3.org/TR/NOTE-datetime">ISO 8601</a>).
27   * <p/>
28   * The currently supported format is:
29   * <pre>
30   *   &plusmn;YYYY-MM-DDThh:mm:ss.SSSTZD
31   * </pre>
32   * where:
33   * <pre>
34   *   &plusmn;YYYY = four-digit year with optional sign where values <= 0 are
35   *           denoting years BCE and values > 0 are denoting years CE,
36   *           e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE,
37   *           0001 denotes the year 1 CE, and so on...
38   *   MM    = two-digit month (01=January, etc.)
39   *   DD    = two-digit day of month (01 through 31)
40   *   hh    = two digits of hour (00 through 23) (am/pm NOT allowed)
41   *   mm    = two digits of minute (00 through 59)
42   *   ss    = two digits of second (00 through 59)
43   *   SSS   = three digits of milliseconds (000 through 999)
44   *   TZD   = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC
45   *           in the form of +hh:mm or -hh:mm
46   * </pre>
47   * <p/>
48   * <em>Note:</em> This class is copied from org.apache.jackrabbit.util.ISO8601
49   */
50  public final class ISO8601 {
51      private ISO8601() { }
52  
53      /**
54       * Parses an ISO8601-compliant date/time string.
55       *
56       * @param text the date/time string to be parsed
57       * @return a <code>Calendar</code>, or <code>null</code> if the input could
58       *         not be parsed
59       * @throws IllegalArgumentException if a <code>null</code> argument is passed
60       */
61      public static Calendar parse(String text) {
62          if (text == null) {
63              throw new IllegalArgumentException("argument can not be null");
64          }
65  
66          // check optional leading sign
67          char sign;
68          int start;
69          if (text.startsWith("-")) {
70              sign = '-';
71              start = 1;
72          } else if (text.startsWith("+")) {
73              sign = '+';
74              start = 1;
75          } else {
76              sign = '+'; // no sign specified, implied '+'
77              start = 0;
78          }
79  
80          /**
81           * the expected format of the remainder of the string is:
82           * YYYY-MM-DDThh:mm:ss.SSSTZD
83           *
84           * note that we cannot use java.text.SimpleDateFormat for
85           * parsing because it can't handle years <= 0 and TZD's
86           */
87  
88          int year, month, day, hour, min, sec, ms;
89          String tzID;
90          try {
91              // year (YYYY)
92              year = Integer.parseInt(text.substring(start, start + 4));
93              start += 4;
94              // delimiter '-'
95              if (text.charAt(start) != '-') {
96                  return null;
97              }
98              start++;
99              // month (MM)
100             month = Integer.parseInt(text.substring(start, start + 2));
101             start += 2;
102             // delimiter '-'
103             if (text.charAt(start) != '-') {
104                 return null;
105             }
106             start++;
107             // day (DD)
108             day = Integer.parseInt(text.substring(start, start + 2));
109             start += 2;
110             // delimiter 'T'
111             if (text.charAt(start) != 'T') {
112                 return null;
113             }
114             start++;
115             // hour (hh)
116             hour = Integer.parseInt(text.substring(start, start + 2));
117             start += 2;
118             // delimiter ':'
119             if (text.charAt(start) != ':') {
120                 return null;
121             }
122             start++;
123             // minute (mm)
124             min = Integer.parseInt(text.substring(start, start + 2));
125             start += 2;
126             // delimiter ':'
127             if (text.charAt(start) != ':') {
128                 return null;
129             }
130             start++;
131             // second (ss)
132             sec = Integer.parseInt(text.substring(start, start + 2));
133             start += 2;
134             // delimiter '.'
135             if (text.charAt(start) != '.') {
136                 return null;
137             }
138             start++;
139             // millisecond (SSS)
140             ms = Integer.parseInt(text.substring(start, start + 3));
141             start += 3;
142             // time zone designator (Z or +00:00 or -00:00)
143             if (text.charAt(start) == '+' || text.charAt(start) == '-') {
144                 // offset to UTC specified in the format +00:00/-00:00
145                 tzID = "GMT" + text.substring(start);
146             } else if (text.substring(start).equals("Z")) {
147                 tzID = "GMT";
148             } else {
149                 // invalid time zone designator
150                 return null;
151             }
152         } catch (IndexOutOfBoundsException e) {
153             return null;
154         } catch (NumberFormatException e) {
155             return null;
156         }
157 
158         TimeZone tz = TimeZone.getTimeZone(tzID);
159         // verify id of returned time zone (getTimeZone defaults to "GMT")
160         if (!tz.getID().equals(tzID)) {
161             // invalid time zone
162             return null;
163         }
164 
165         // initialize Calendar object
166         Calendar cal = Calendar.getInstance(tz);
167         cal.setLenient(false);
168         // year and era
169         if (sign == '-' || year == 0) {
170             // not CE, need to set era (BCE) and adjust year
171             cal.set(Calendar.YEAR, year + 1);
172             cal.set(Calendar.ERA, GregorianCalendar.BC);
173         } else {
174             cal.set(Calendar.YEAR, year);
175             cal.set(Calendar.ERA, GregorianCalendar.AD);
176         }
177         // month (0-based!)
178         cal.set(Calendar.MONTH, month - 1);
179         // day of month
180         cal.set(Calendar.DAY_OF_MONTH, day);
181         // hour
182         cal.set(Calendar.HOUR_OF_DAY, hour);
183         // minute
184         cal.set(Calendar.MINUTE, min);
185         // second
186         cal.set(Calendar.SECOND, sec);
187         // millisecond
188         cal.set(Calendar.MILLISECOND, ms);
189 
190         try {
191             /**
192              * the following call will trigger an IllegalArgumentException
193              * if any of the set values are illegal or out of range
194              */
195             cal.getTime();
196             /**
197              * in addition check the validity of the year
198              */
199             getYear(cal);
200         } catch (IllegalArgumentException e) {
201             return null;
202         }
203 
204         return cal;
205     }
206 
207     /**
208      * Formats a <code>Calendar</code> value into an ISO8601-compliant
209      * date/time string.
210      *
211      * @param cal the time value to be formatted into a date/time string.
212      * @return the formatted date/time string.
213      * @throws IllegalArgumentException if a <code>null</code> argument is passed
214      * or the calendar cannot be represented as defined by ISO 8601 (i.e. year
215      * with more than four digits).
216      */
217     public static String format(Calendar cal) throws IllegalArgumentException {
218         if (cal == null) {
219             throw new IllegalArgumentException("argument can not be null");
220         }
221 
222         /**
223          * the format of the date/time string is:
224          * YYYY-MM-DDThh:mm:ss.SSSTZD
225          *
226          * note that we cannot use java.text.SimpleDateFormat for
227          * formatting because it can't handle years <= 0 and TZD's
228          */
229         StringBuffer buf = new StringBuffer();
230         // year ([-]YYYY)
231         appendZeroPaddedInt(buf, getYear(cal), 4);
232         buf.append('-');
233         // month (MM)
234         appendZeroPaddedInt(buf, cal.get(Calendar.MONTH) + 1, 2);
235         buf.append('-');
236         // day (DD)
237         appendZeroPaddedInt(buf, cal.get(Calendar.DAY_OF_MONTH), 2);
238         buf.append('T');
239         // hour (hh)
240         appendZeroPaddedInt(buf, cal.get(Calendar.HOUR_OF_DAY), 2);
241         buf.append(':');
242         // minute (mm)
243         appendZeroPaddedInt(buf, cal.get(Calendar.MINUTE), 2);
244         buf.append(':');
245         // second (ss)
246         appendZeroPaddedInt(buf, cal.get(Calendar.SECOND), 2);
247         buf.append('.');
248         // millisecond (SSS)
249         appendZeroPaddedInt(buf, cal.get(Calendar.MILLISECOND), 3);
250         // time zone designator (Z or +00:00 or -00:00)
251         TimeZone tz = cal.getTimeZone();
252         // determine offset of timezone from UTC (incl. daylight saving)
253         int offset = tz.getOffset(cal.getTimeInMillis());
254         if (offset == 0) {
255             buf.append('Z');
256         } 
257         else {
258             int hours = Math.abs(offset / (60 * 1000) / 60);
259             int minutes = Math.abs(offset / (60 * 1000) % 60);
260             buf.append(offset < 0 ? '-' : '+');
261             appendZeroPaddedInt(buf, hours, 2);
262             buf.append(':');
263             appendZeroPaddedInt(buf, minutes, 2);
264         }
265         return buf.toString();
266     }
267 
268     /**
269      * Returns the astronomical year of the given calendar.
270      *
271      * @param cal a calendar instance.
272      * @return the astronomical year.
273      * @throws IllegalArgumentException if calendar cannot be represented as
274      *                                  defined by ISO 8601 (i.e. year with more
275      *                                  than four digits).
276      */
277     public static int getYear(Calendar cal) throws IllegalArgumentException {
278         // determine era and adjust year if necessary
279         int year = cal.get(Calendar.YEAR);
280         if (cal.isSet(Calendar.ERA)
281                 && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
282 
283             // calculate year using astronomical system:
284             // year n BCE => astronomical year -n + 1
285             year = 0 - year + 1;
286         }
287 
288         if (year > 9999 || year < -9999) {
289             throw new IllegalArgumentException("Calendar has more than four " +
290                     "year digits, cannot be formatted as ISO8601: " + year);
291         }
292         return year;
293     }
294 
295     /**
296      * Appends a zero-padded number to the given string buffer.
297      * <p/>
298      * This is an internal helper method which doesn't perform any
299      * validation on the given arguments.
300      *
301      * @param buf String buffer to append to
302      * @param n number to append
303      * @param precision number of digits to append
304      */
305     private static void appendZeroPaddedInt(StringBuffer buf, int n, int precision) {
306         if (n < 0) {
307             buf.append('-');
308             n = -n;
309         }
310 
311         for (int exp = precision - 1; exp > 0; exp--) {
312             if (n < Math.pow(10, exp)) {
313                 buf.append('0');
314             } else {
315                 break;
316             }
317         }
318         buf.append(n);
319     }
320 }