diff options
Diffstat (limited to 'java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java')
-rw-r--r-- | java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java | 503 |
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); + } + } +} + + |