diff --git a/src/org/apache/jackrabbit/util/ISO8601.java b/src/org/apache/jackrabbit/util/ISO8601.java new file mode 100644 index 0000000..3f3c85a --- /dev/null +++ b/src/org/apache/jackrabbit/util/ISO8601.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * The ISO8601 utility class provides helper methods + * to deal with date/time formatting using a specific ISO8601-compliant + * format (see ISO 8601). + *

+ * The currently supported format is: + *

+ *   ±YYYY-MM-DDThh:mm:ss.SSSTZD
+ * 
+ * where: + *
+ *   ±YYYY = four-digit year with optional sign where values <= 0 are
+ *           denoting years BCE and values > 0 are denoting years CE,
+ *           e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE,
+ *           0001 denotes the year 1 CE, and so on...
+ *   MM    = two-digit month (01=January, etc.)
+ *   DD    = two-digit day of month (01 through 31)
+ *   hh    = two digits of hour (00 through 23) (am/pm NOT allowed)
+ *   mm    = two digits of minute (00 through 59)
+ *   ss    = two digits of second (00 through 59)
+ *   SSS   = three digits of milliseconds (000 through 999)
+ *   TZD   = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC
+ *           in the form of +hh:mm or -hh:mm
+ * 
+ */ +public final class ISO8601 { + /** + * Parses an ISO8601-compliant date/time string. + * + * @param text the date/time string to be parsed + * @return a Calendar, or null if the input could + * not be parsed + * @throws IllegalArgumentException if a null argument is passed + */ + public static Calendar parse(String text) { + if (text == null) { + throw new IllegalArgumentException("argument can not be null"); + } + + // check optional leading sign + char sign; + int start; + if (text.startsWith("-")) { + sign = '-'; + start = 1; + } else if (text.startsWith("+")) { + sign = '+'; + start = 1; + } else { + sign = '+'; // no sign specified, implied '+' + start = 0; + } + + /** + * the expected format of the remainder of the string is: + * YYYY-MM-DDThh:mm:ss.SSSTZD + * + * note that we cannot use java.text.SimpleDateFormat for + * parsing because it can't handle years <= 0 and TZD's + */ + + int year, month, day, hour, min, sec, ms; + String tzID; + try { + // year (YYYY) + year = Integer.parseInt(text.substring(start, start + 4)); + start += 4; + // delimiter '-' + if (text.charAt(start) != '-') { + return null; + } + start++; + // month (MM) + month = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter '-' + if (text.charAt(start) != '-') { + return null; + } + start++; + // day (DD) + day = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter 'T' + if (text.charAt(start) != 'T') { + return null; + } + start++; + // hour (hh) + hour = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (text.charAt(start) != ':') { + return null; + } + start++; + // minute (mm) + min = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (text.charAt(start) != ':') { + return null; + } + start++; + // second (ss) + sec = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter '.' + if (text.charAt(start) != '.') { + return null; + } + start++; + // millisecond (SSS) + ms = Integer.parseInt(text.substring(start, start + 3)); + start += 3; + // time zone designator (Z or +00:00 or -00:00) + if (text.charAt(start) == '+' || text.charAt(start) == '-') { + // offset to UTC specified in the format +00:00/-00:00 + tzID = "GMT" + text.substring(start); + } else if (text.substring(start).equals("Z")) { + tzID = "GMT"; + } else { + // invalid time zone designator + return null; + } + } catch (IndexOutOfBoundsException e) { + return null; + } catch (NumberFormatException e) { + return null; + } + + TimeZone tz = TimeZone.getTimeZone(tzID); + // verify id of returned time zone (getTimeZone defaults to "GMT") + if (!tz.getID().equals(tzID)) { + // invalid time zone + return null; + } + + // initialize Calendar object + Calendar cal = Calendar.getInstance(tz); + cal.setLenient(false); + // year and era + if (sign == '-' || year == 0) { + // not CE, need to set era (BCE) and adjust year + cal.set(Calendar.YEAR, year + 1); + cal.set(Calendar.ERA, GregorianCalendar.BC); + } else { + cal.set(Calendar.YEAR, year); + cal.set(Calendar.ERA, GregorianCalendar.AD); + } + // month (0-based!) + cal.set(Calendar.MONTH, month - 1); + // day of month + cal.set(Calendar.DAY_OF_MONTH, day); + // hour + cal.set(Calendar.HOUR_OF_DAY, hour); + // minute + cal.set(Calendar.MINUTE, min); + // second + cal.set(Calendar.SECOND, sec); + // millisecond + cal.set(Calendar.MILLISECOND, ms); + + try { + /** + * the following call will trigger an IllegalArgumentException + * if any of the set values are illegal or out of range + */ + cal.getTime(); + /** + * in addition check the validity of the year + */ + getYear(cal); + } catch (IllegalArgumentException e) { + return null; + } + + return cal; + } + + /** + * Formats a Calendar value into an ISO8601-compliant + * date/time string. + * + * @param cal the time value to be formatted into a date/time string. + * @return the formatted date/time string. + * @throws IllegalArgumentException if a null argument is passed + * or the calendar cannot be represented as defined by ISO 8601 (i.e. year + * with more than four digits). + */ + public static String format(Calendar cal) throws IllegalArgumentException { + if (cal == null) { + throw new IllegalArgumentException("argument can not be null"); + } + + /** + * the format of the date/time string is: + * YYYY-MM-DDThh:mm:ss.SSSTZD + * + * note that we cannot use java.text.SimpleDateFormat for + * formatting because it can't handle years <= 0 and TZD's + */ + StringBuffer buf = new StringBuffer(); + // year ([-]YYYY) + appendZeroPaddedInt(buf, getYear(cal), 4); + buf.append('-'); + // month (MM) + appendZeroPaddedInt(buf, cal.get(Calendar.MONTH) + 1, 2); + buf.append('-'); + // day (DD) + appendZeroPaddedInt(buf, cal.get(Calendar.DAY_OF_MONTH), 2); + buf.append('T'); + // hour (hh) + appendZeroPaddedInt(buf, cal.get(Calendar.HOUR_OF_DAY), 2); + buf.append(':'); + // minute (mm) + appendZeroPaddedInt(buf, cal.get(Calendar.MINUTE), 2); + buf.append(':'); + // second (ss) + appendZeroPaddedInt(buf, cal.get(Calendar.SECOND), 2); + buf.append('.'); + // millisecond (SSS) + appendZeroPaddedInt(buf, cal.get(Calendar.MILLISECOND), 3); + // time zone designator (Z or +00:00 or -00:00) + TimeZone tz = cal.getTimeZone(); + // determine offset of timezone from UTC (incl. daylight saving) + int offset = tz.getOffset(cal.getTimeInMillis()); + if (offset != 0) { + int hours = Math.abs((offset / (60 * 1000)) / 60); + int minutes = Math.abs((offset / (60 * 1000)) % 60); + buf.append(offset < 0 ? '-' : '+'); + appendZeroPaddedInt(buf, hours, 2); + buf.append(':'); + appendZeroPaddedInt(buf, minutes, 2); + } else { + buf.append('Z'); + } + return buf.toString(); + } + + /** + * Returns the astronomical year of the given calendar. + * + * @param cal a calendar instance. + * @return the astronomical year. + * @throws IllegalArgumentException if calendar cannot be represented as + * defined by ISO 8601 (i.e. year with more + * than four digits). + */ + public static int getYear(Calendar cal) throws IllegalArgumentException { + // determine era and adjust year if necessary + int year = cal.get(Calendar.YEAR); + if (cal.isSet(Calendar.ERA) + && cal.get(Calendar.ERA) == GregorianCalendar.BC) { + /** + * calculate year using astronomical system: + * year n BCE => astronomical year -n + 1 + */ + year = 0 - year + 1; + } + + if (year > 9999 || year < -9999) { + throw new IllegalArgumentException("Calendar has more than four " + + "year digits, cannot be formatted as ISO8601: " + year); + } + return year; + } + + /** + * Appends a zero-padded number to the given string buffer. + *

+ * This is an internal helper method which doesn't perform any + * validation on the given arguments. + * + * @param buf String buffer to append to + * @param n number to append + * @param precision number of digits to append + */ + private static void appendZeroPaddedInt(StringBuffer buf, int n, int precision) { + if (n < 0) { + buf.append('-'); + n = -n; + } + + for (int exp = precision - 1; exp > 0; exp--) { + if (n < Math.pow(10, exp)) { + buf.append('0'); + } else { + break; + } + } + buf.append(n); + } +} diff --git a/src/org/apache/jackrabbit/util/note b/src/org/apache/jackrabbit/util/note new file mode 100644 index 0000000..558093b --- /dev/null +++ b/src/org/apache/jackrabbit/util/note @@ -0,0 +1 @@ +Extract from jackrabbit-2.1.1-src.zip \ No newline at end of file