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 * ±YYYY-MM-DDThh:mm:ss.SSSTZD
31 * </pre>
32 * where:
33 * <pre>
34 * ±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 }