summaryrefslogtreecommitdiff
path: root/java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java')
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java503
1 files changed, 503 insertions, 0 deletions
diff --git a/java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java b/java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java
new file mode 100644
index 0000000..2d1939d
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java
@@ -0,0 +1,503 @@
+// =================================================================================================
+// ADOBE SYSTEMS INCORPORATED
+// Copyright 2006-2007 Adobe Systems Incorporated
+// All Rights Reserved
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms
+// of the Adobe license agreement accompanying it.
+// =================================================================================================
+
+package com.adobe.xmp.impl;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+
+import com.adobe.xmp.XMPDateTime;
+import com.adobe.xmp.XMPError;
+import com.adobe.xmp.XMPException;
+
+
+/**
+ * Converts between ISO 8601 Strings and <code>Calendar</code> with millisecond resolution.
+ *
+ * @since 16.02.2006
+ */
+public final class ISO8601Converter
+{
+ /** Hides public constructor */
+ private ISO8601Converter()
+ {
+ // EMPTY
+ }
+
+
+ /**
+ * Converts an ISO 8601 string to an <code>XMPDateTime</code>.
+ *
+ * Parse a date according to ISO 8601 and
+ * http://www.w3.org/TR/NOTE-datetime:
+ * <ul>
+ * <li>YYYY
+ * <li>YYYY-MM
+ * <li>YYYY-MM-DD
+ * <li>YYYY-MM-DDThh:mmTZD
+ * <li>YYYY-MM-DDThh:mm:ssTZD
+ * <li>YYYY-MM-DDThh:mm:ss.sTZD
+ * </ul>
+ *
+ * Data fields:
+ * <ul>
+ * <li>YYYY = four-digit year
+ * <li>MM = two-digit month (01=January, etc.)
+ * <li>DD = two-digit day of month (01 through 31)
+ * <li>hh = two digits of hour (00 through 23)
+ * <li>mm = two digits of minute (00 through 59)
+ * <li>ss = two digits of second (00 through 59)
+ * <li>s = one or more digits representing a decimal fraction of a second
+ * <li>TZD = time zone designator (Z or +hh:mm or -hh:mm)
+ * </ul>
+ *
+ * Note that ISO 8601 does not seem to allow years less than 1000 or greater
+ * than 9999. We allow any year, even negative ones. The year is formatted
+ * as "%.4d".
+ * <p>
+ * <em>Note:</em> Tolerate missing TZD, assume is UTC. Photoshop 8 writes
+ * dates like this for exif:GPSTimeStamp.<br>
+ * <em>Note:</em> Tolerate missing date portion, in case someone foolishly
+ * writes a time-only value that way.
+ *
+ * @param iso8601String a date string that is ISO 8601 conform.
+ * @return Returns a <code>Calendar</code>.
+ * @throws XMPException Is thrown when the string is non-conform.
+ */
+ public static XMPDateTime parse(String iso8601String) throws XMPException
+ {
+ return parse(iso8601String, new XMPDateTimeImpl());
+ }
+
+
+ /**
+ * @param iso8601String a date string that is ISO 8601 conform.
+ * @param binValue an existing XMPDateTime to set with the parsed date
+ * @return Returns an XMPDateTime-object containing the ISO8601-date.
+ * @throws XMPException Is thrown when the string is non-conform.
+ */
+ public static XMPDateTime parse(String iso8601String, XMPDateTime binValue) throws XMPException
+ {
+ ParameterAsserts.assertNotNull(iso8601String);
+
+ ParseState input = new ParseState(iso8601String);
+ int value;
+
+ boolean timeOnly =
+ input.ch(0) == 'T' ||
+ (input.length() >= 2 && input.ch(1) == ':' ||
+ (input.length() >= 3 && input.ch(2) == ':'));
+
+ if (!timeOnly)
+ {
+ if (input.ch(0) == '-')
+ {
+ input.skip();
+ }
+
+
+ // Extract the year.
+ value = input.gatherInt("Invalid year in date string", 9999);
+ if (input.hasNext() && input.ch() != '-')
+ {
+ throw new XMPException("Invalid date string, after year", XMPError.BADVALUE);
+ }
+
+ if (input.ch(0) == '-')
+ {
+ value = -value;
+ }
+ binValue.setYear(value);
+ if (!input.hasNext())
+ {
+ return binValue;
+ }
+ input.skip();
+
+
+ // Extract the month.
+ value = input.gatherInt("Invalid month in date string", 12);
+ if (input.hasNext() && input.ch() != '-')
+ {
+ throw new XMPException("Invalid date string, after month", XMPError.BADVALUE);
+ }
+ binValue.setMonth(value);
+ if (!input.hasNext())
+ {
+ return binValue;
+ }
+ input.skip();
+
+
+ // Extract the day.
+ value = input.gatherInt("Invalid day in date string", 31);
+ if (input.hasNext() && input.ch() != 'T')
+ {
+ throw new XMPException("Invalid date string, after day", XMPError.BADVALUE);
+ }
+ binValue.setDay(value);
+ if (!input.hasNext())
+ {
+ return binValue;
+ }
+ }
+ else
+ {
+ // set default day and month in the year 0000
+ binValue.setMonth(1);
+ binValue.setDay(1);
+ }
+
+ if (input.ch() == 'T')
+ {
+ input.skip();
+ }
+ else if (!timeOnly)
+ {
+ throw new XMPException("Invalid date string, missing 'T' after date",
+ XMPError.BADVALUE);
+ }
+
+
+ // Extract the hour.
+ value = input.gatherInt("Invalid hour in date string", 23);
+ if (input.ch() != ':')
+ {
+ throw new XMPException("Invalid date string, after hour", XMPError.BADVALUE);
+ }
+ binValue.setHour(value);
+
+ // Don't check for done, we have to work up to the time zone.
+ input.skip();
+
+
+ // Extract the minute.
+ value = input.gatherInt("Invalid minute in date string", 59);
+ if (input.hasNext() &&
+ input.ch() != ':' && input.ch() != 'Z' && input.ch() != '+' && input.ch() != '-')
+ {
+ throw new XMPException("Invalid date string, after minute", XMPError.BADVALUE);
+ }
+ binValue.setMinute(value);
+
+ if (input.ch() == ':')
+ {
+ input.skip();
+ value = input.gatherInt("Invalid whole seconds in date string", 59);
+ if (input.hasNext() && input.ch() != '.' && input.ch() != 'Z' &&
+ input.ch() != '+' && input.ch() != '-')
+ {
+ throw new XMPException("Invalid date string, after whole seconds",
+ XMPError.BADVALUE);
+ }
+ binValue.setSecond(value);
+ if (input.ch() == '.')
+ {
+ input.skip();
+ int digits = input.pos();
+ value = input.gatherInt("Invalid fractional seconds in date string", 999999999);
+ if (input.ch() != 'Z' && input.ch() != '+' && input.ch() != '-')
+ {
+ throw new XMPException("Invalid date string, after fractional second",
+ XMPError.BADVALUE);
+ }
+ digits = input.pos() - digits;
+ for (; digits > 9; --digits)
+ {
+ value = value / 10;
+ }
+ for (; digits < 9; ++digits)
+ {
+ value = value * 10;
+ }
+ binValue.setNanoSecond(value);
+ }
+ }
+
+ int tzSign = 0;
+ int tzHour = 0;
+ int tzMinute = 0;
+ if (input.ch() == 'Z')
+ {
+ input.skip();
+ }
+ else if (input.hasNext())
+ {
+ if (input.ch() == '+')
+ {
+ tzSign = 1;
+ }
+ else if (input.ch() == '-')
+ {
+ tzSign = -1;
+ }
+ else
+ {
+ throw new XMPException("Time zone must begin with 'Z', '+', or '-'",
+ XMPError.BADVALUE);
+ }
+
+ input.skip();
+ // Extract the time zone hour.
+ tzHour = input.gatherInt("Invalid time zone hour in date string", 23);
+ if (input.ch() != ':')
+ {
+ throw new XMPException("Invalid date string, after time zone hour",
+ XMPError.BADVALUE);
+ }
+ input.skip();
+
+ // Extract the time zone minute.
+ tzMinute = input.gatherInt("Invalid time zone minute in date string", 59);
+ }
+
+ // create a corresponding TZ and set it time zone
+ int offset = (tzHour * 3600 * 1000 + tzMinute * 60 * 1000) * tzSign;
+ binValue.setTimeZone(new SimpleTimeZone(offset, ""));
+
+
+ if (input.hasNext())
+ {
+ throw new XMPException(
+ "Invalid date string, extra chars at end", XMPError.BADVALUE);
+ }
+
+ return binValue;
+ }
+
+
+ /**
+ * Converts a <code>Calendar</code> into an ISO 8601 string.
+ * Format a date according to ISO 8601 and http://www.w3.org/TR/NOTE-datetime:
+ * <ul>
+ * <li>YYYY
+ * <li>YYYY-MM
+ * <li>YYYY-MM-DD
+ * <li>YYYY-MM-DDThh:mmTZD
+ * <li>YYYY-MM-DDThh:mm:ssTZD
+ * <li>YYYY-MM-DDThh:mm:ss.sTZD
+ * </ul>
+ *
+ * Data fields:
+ * <ul>
+ * <li>YYYY = four-digit year
+ * <li>MM = two-digit month (01=January, etc.)
+ * <li>DD = two-digit day of month (01 through 31)
+ * <li>hh = two digits of hour (00 through 23)
+ * <li>mm = two digits of minute (00 through 59)
+ * <li>ss = two digits of second (00 through 59)
+ * <li>s = one or more digits representing a decimal fraction of a second
+ * <li>TZD = time zone designator (Z or +hh:mm or -hh:mm)
+ * </ul>
+ * <p>
+ * <em>Note:</em> ISO 8601 does not seem to allow years less than 1000 or greater than 9999.
+ * We allow any year, even negative ones. The year is formatted as "%.4d".<p>
+ * <em>Note:</em> Fix for bug 1269463 (silently fix out of range values) included in parsing.
+ * The quasi-bogus "time only" values from Photoshop CS are not supported.
+ *
+ * @param dateTime an XMPDateTime-object.
+ * @return Returns an ISO 8601 string.
+ */
+ public static String render(XMPDateTime dateTime)
+ {
+ StringBuffer buffer = new StringBuffer();
+
+ // year is rendered in any case, even 0000
+ DecimalFormat df = new DecimalFormat("0000", new DecimalFormatSymbols(Locale.ENGLISH));
+ buffer.append(df.format(dateTime.getYear()));
+ if (dateTime.getMonth() == 0)
+ {
+ return buffer.toString();
+ }
+
+ // month
+ df.applyPattern("'-'00");
+ buffer.append(df.format(dateTime.getMonth()));
+ if (dateTime.getDay() == 0)
+ {
+ return buffer.toString();
+ }
+
+ // day
+ buffer.append(df.format(dateTime.getDay()));
+
+ // time, rendered if any time field is not zero
+ if (dateTime.getHour() != 0 ||
+ dateTime.getMinute() != 0 ||
+ dateTime.getSecond() != 0 ||
+ dateTime.getNanoSecond() != 0 ||
+ (dateTime.getTimeZone() != null && dateTime.getTimeZone().getRawOffset() != 0))
+ {
+ // hours and minutes
+ buffer.append('T');
+ df.applyPattern("00");
+ buffer.append(df.format(dateTime.getHour()));
+ buffer.append(':');
+ buffer.append(df.format(dateTime.getMinute()));
+
+ // seconds and nanoseconds
+ if (dateTime.getSecond() != 0 || dateTime.getNanoSecond() != 0)
+ {
+ double seconds = dateTime.getSecond() + dateTime.getNanoSecond() / 1e9d;
+
+ df.applyPattern(":00.#########");
+ buffer.append(df.format(seconds));
+ }
+
+ // time zone
+ if (dateTime.getTimeZone() != null)
+ {
+ if (dateTime.getTimeZone().getRawOffset() == 0)
+ {
+ // UTC
+ buffer.append('Z');
+ }
+ else
+ {
+ int offset = dateTime.getTimeZone().getRawOffset();
+ int thours = offset / 3600000;
+ int tminutes = Math.abs(offset % 3600000 / 60000);
+ df.applyPattern("+00;-00");
+ buffer.append(df.format(thours));
+ df.applyPattern(":00");
+ buffer.append(df.format(tminutes));
+ }
+ }
+ }
+ return buffer.toString();
+ }
+
+
+}
+
+
+/**
+ * @since 22.08.2006
+ */
+class ParseState
+{
+ /** */
+ private String str;
+ /** */
+ private int pos = 0;
+
+
+ /**
+ * @param str initializes the parser container
+ */
+ public ParseState(String str)
+ {
+ this.str = str;
+ }
+
+
+ /**
+ * @return Returns the length of the input.
+ */
+ public int length()
+ {
+ return str.length();
+ }
+
+
+ /**
+ * @return Returns whether there are more chars to come.
+ */
+ public boolean hasNext()
+ {
+ return pos < str.length();
+ }
+
+
+ /**
+ * @param index index of char
+ * @return Returns char at a certain index.
+ */
+ public char ch(int index)
+ {
+ return index < str.length() ?
+ str.charAt(index) :
+ 0x0000;
+ }
+
+
+ /**
+ * @return Returns the current char or 0x0000 if there are no more chars.
+ */
+ public char ch()
+ {
+ return pos < str.length() ?
+ str.charAt(pos) :
+ 0x0000;
+ }
+
+
+ /**
+ * Skips the next char.
+ */
+ public void skip()
+ {
+ pos++;
+ }
+
+
+ /**
+ * @return Returns the current position.
+ */
+ public int pos()
+ {
+ return pos;
+ }
+
+
+ /**
+ * Parses a integer from the source and sets the pointer after it.
+ * @param errorMsg Error message to put in the exception if no number can be found
+ * @param maxValue the max value of the number to return
+ * @return Returns the parsed integer.
+ * @throws XMPException Thrown if no integer can be found.
+ */
+ public int gatherInt(String errorMsg, int maxValue) throws XMPException
+ {
+ int value = 0;
+ boolean success = false;
+ char ch = ch(pos);
+ while ('0' <= ch && ch <= '9')
+ {
+ value = (value * 10) + (ch - '0');
+ success = true;
+ pos++;
+ ch = ch(pos);
+ }
+
+ if (success)
+ {
+ if (value > maxValue)
+ {
+ return maxValue;
+ }
+ else if (value < 0)
+ {
+ return 0;
+ }
+ else
+ {
+ return value;
+ }
+ }
+ else
+ {
+ throw new XMPException(errorMsg, XMPError.BADVALUE);
+ }
+ }
+}
+
+