content: // =================================================================================================
  // ADOBE SYSTEMS INCORPORATED
  // Copyright 2006 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.internal.xmp.impl;
  
  import java.text.DecimalFormat;
  import java.text.DecimalFormatSymbols;
  import java.util.Locale;
  import java.util.SimpleTimeZone;
  
  import com.adobe.internal.xmp.XMPDateTime;
  import com.adobe.internal.xmp.XMPError;
  import com.adobe.internal.xmp.XMPException;
  
  
  /**
   * Converts between ISO 8601 Strings and <code>Calendar</code> with millisecond resolution.
   *
   * @author  Stefan Makswit
   * @version $Revision$
   * @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
  	 * https://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> DOES NOT APPLY ANYMORE.
  	 * 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
  	{
  		if (iso8601String == null)
  		{
  			throw new XMPException("Parameter must not be null", XMPError.BADPARAM);
  		}
  		else if (iso8601String.length() == 0)
  		{
  			return binValue;
  		}
  
  		ParseState input = new ParseState(iso8601String);
  		int value;
  
  		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;
  		}
  		input.skip();
  
  		// Extract the hour.
  		value = input.gatherInt("Invalid hour in date string", 23);
  		binValue.setHour(value);
  		if (!input.hasNext())
  		{
  			return binValue;
  		}
  
  		// Extract the minute.
  		if (input.ch() == ':')
  		{
  			input.skip();
  			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.hasNext())
  		{
  			return binValue;
  		}
  		else if (input.hasNext()  &&  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.hasNext()  &&
  					(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);
  			}
  		}
  		else if (input.ch() != 'Z'  &&  input.ch() != '+'  &&  input.ch() != '-')
  		{
  			throw new XMPException("Invalid date string, after time", XMPError.BADVALUE);
  		}
  
  
  		int tzSign = 0;
  		int tzHour = 0;
  		int tzMinute = 0;
  
  		if (!input.hasNext())
  		{
  			// no Timezone at all
  			return binValue;
  		}
  		else 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.hasNext())
  			{
  				if (input.ch() == ':')
  				{
  					input.skip();
  
  					// Extract the time zone minute.
  					tzMinute = input.gatherInt("Invalid time zone minute in date string", 59);
  				}
  				else
  				{
  					throw new XMPException("Invalid date string, after time zone hour",
  						XMPError.BADVALUE);
  				}
  			}
  		}
  
  		// create a corresponding TZ and set it time zone
  		// FfF: would it be better to map into existing time zones?
  		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 https://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();
  
  		if (dateTime.hasDate())
  		{
  			// 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.hasTime())
  			{
  				// 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.hasTimeZone())
  				{
  					// used to calculate the time zone offset incl. Daylight Savings
  					long timeInMillis = dateTime.getCalendar().getTimeInMillis();
  					int offset = dateTime.getTimeZone().getOffset(timeInMillis);
  					if (offset == 0)
  					{
  						// UTC
  						buffer.append('Z');
  					}
  					else
  					{
  						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();
  	}
  
  
  }
  
  
  /**
   * @author  Stefan Makswit
   * @version $Revision$
   * @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);
  		}
  	}
  }
  
  
  