Home | History | Annotate | Download | only in impl
      1 // =================================================================================================
      2 // ADOBE SYSTEMS INCORPORATED
      3 // Copyright 2006 Adobe Systems Incorporated
      4 // All Rights Reserved
      5 //
      6 // NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the terms
      7 // of the Adobe license agreement accompanying it.
      8 // =================================================================================================
      9 
     10 package com.adobe.xmp.impl;
     11 
     12 import java.text.DecimalFormat;
     13 import java.text.DecimalFormatSymbols;
     14 import java.util.Locale;
     15 import java.util.SimpleTimeZone;
     16 
     17 import com.adobe.xmp.XMPDateTime;
     18 import com.adobe.xmp.XMPError;
     19 import com.adobe.xmp.XMPException;
     20 
     21 
     22 /**
     23  * Converts between ISO 8601 Strings and <code>Calendar</code> with millisecond resolution.
     24  *
     25  * @since   16.02.2006
     26  */
     27 public final class ISO8601Converter
     28 {
     29 	/** Hides public constructor */
     30 	private ISO8601Converter()
     31 	{
     32 		// EMPTY
     33 	}
     34 
     35 
     36 	/**
     37 	 * Converts an ISO 8601 string to an <code>XMPDateTime</code>.
     38 	 *
     39 	 * Parse a date according to ISO 8601 and
     40 	 * http://www.w3.org/TR/NOTE-datetime:
     41 	 * <ul>
     42 	 * <li>YYYY
     43 	 * <li>YYYY-MM
     44 	 * <li>YYYY-MM-DD
     45 	 * <li>YYYY-MM-DDThh:mmTZD
     46 	 * <li>YYYY-MM-DDThh:mm:ssTZD
     47 	 * <li>YYYY-MM-DDThh:mm:ss.sTZD
     48 	 * </ul>
     49 	 *
     50 	 * Data fields:
     51 	 * <ul>
     52 	 * <li>YYYY = four-digit year
     53 	 * <li>MM = two-digit month (01=January, etc.)
     54 	 * <li>DD = two-digit day of month (01 through 31)
     55 	 * <li>hh = two digits of hour (00 through 23)
     56 	 * <li>mm = two digits of minute (00 through 59)
     57 	 * <li>ss = two digits of second (00 through 59)
     58 	 * <li>s = one or more digits representing a decimal fraction of a second
     59 	 * <li>TZD = time zone designator (Z or +hh:mm or -hh:mm)
     60 	 * </ul>
     61 	 *
     62 	 * Note that ISO 8601 does not seem to allow years less than 1000 or greater
     63 	 * than 9999. We allow any year, even negative ones. The year is formatted
     64 	 * as "%.4d".
     65 	 * <p>
     66 	 * <em>Note:</em> Tolerate missing TZD, assume is UTC. Photoshop 8 writes
     67 	 * dates like this for exif:GPSTimeStamp.<br>
     68 	 * <em>Note:</em> Tolerate missing date portion, in case someone foolishly
     69 	 * writes a time-only value that way.
     70 	 *
     71 	 * @param iso8601String a date string that is ISO 8601 conform.
     72 	 * @return Returns a <code>Calendar</code>.
     73 	 * @throws XMPException Is thrown when the string is non-conform.
     74 	 */
     75 	public static XMPDateTime parse(String iso8601String) throws XMPException
     76 	{
     77 		return parse(iso8601String, new XMPDateTimeImpl());
     78 	}
     79 
     80 
     81 	/**
     82 	 * @param iso8601String a date string that is ISO 8601 conform.
     83 	 * @param binValue an existing XMPDateTime to set with the parsed date
     84 	 * @return Returns an XMPDateTime-object containing the ISO8601-date.
     85 	 * @throws XMPException Is thrown when the string is non-conform.
     86 	 */
     87 	public static XMPDateTime parse(String iso8601String, XMPDateTime binValue) throws XMPException
     88 	{
     89 		ParameterAsserts.assertNotNull(iso8601String);
     90 
     91 		ParseState input = new ParseState(iso8601String);
     92 		int value;
     93 
     94 		boolean timeOnly =
     95 			 input.ch(0) == 'T'  ||
     96 			(input.length() >= 2  &&  input.ch(1) == ':'  ||
     97 			(input.length() >= 3  &&  input.ch(2) == ':'));
     98 
     99 		if (!timeOnly)
    100 		{
    101 			if (input.ch(0) == '-')
    102 			{
    103 				input.skip();
    104 			}
    105 
    106 
    107 			// Extract the year.
    108 			value = input.gatherInt("Invalid year in date string", 9999);
    109 			if (input.hasNext()  &&  input.ch() != '-')
    110 			{
    111 				throw new XMPException("Invalid date string, after year", XMPError.BADVALUE);
    112 			}
    113 
    114 			if (input.ch(0) == '-')
    115 			{
    116 				value = -value;
    117 			}
    118 			binValue.setYear(value);
    119 			if (!input.hasNext())
    120 			{
    121 				return binValue;
    122 			}
    123 			input.skip();
    124 
    125 
    126 			// Extract the month.
    127 			value = input.gatherInt("Invalid month in date string", 12);
    128 			if (input.hasNext()  &&  input.ch() != '-')
    129 			{
    130 				throw new XMPException("Invalid date string, after month", XMPError.BADVALUE);
    131 			}
    132 			binValue.setMonth(value);
    133 			if (!input.hasNext())
    134 			{
    135 				return binValue;
    136 			}
    137 			input.skip();
    138 
    139 
    140 			// Extract the day.
    141 			value = input.gatherInt("Invalid day in date string", 31);
    142 			if (input.hasNext()  &&  input.ch() != 'T')
    143 			{
    144 				throw new XMPException("Invalid date string, after day", XMPError.BADVALUE);
    145 			}
    146 			binValue.setDay(value);
    147 			if (!input.hasNext())
    148 			{
    149 				return binValue;
    150 			}
    151 		}
    152 		else
    153 		{
    154 			// set default day and month in the year 0000
    155 			binValue.setMonth(1);
    156 			binValue.setDay(1);
    157 		}
    158 
    159 		if (input.ch() == 'T')
    160 		{
    161 			input.skip();
    162 		}
    163 		else if (!timeOnly)
    164 		{
    165 			throw new XMPException("Invalid date string, missing 'T' after date",
    166 					XMPError.BADVALUE);
    167 		}
    168 
    169 
    170 		// Extract the hour.
    171 		value = input.gatherInt("Invalid hour in date string", 23);
    172 		if (input.ch() != ':')
    173 		{
    174 			throw new XMPException("Invalid date string, after hour", XMPError.BADVALUE);
    175 		}
    176 		binValue.setHour(value);
    177 
    178 		// Don't check for done, we have to work up to the time zone.
    179 		input.skip();
    180 
    181 
    182 		// Extract the minute.
    183 		value = input.gatherInt("Invalid minute in date string", 59);
    184 		if (input.hasNext()  &&
    185 			input.ch() != ':' && input.ch() != 'Z' && input.ch() != '+' && input.ch() != '-')
    186 		{
    187 			throw new XMPException("Invalid date string, after minute", XMPError.BADVALUE);
    188 		}
    189 		binValue.setMinute(value);
    190 
    191 		if (input.ch() == ':')
    192 		{
    193 			input.skip();
    194 			value = input.gatherInt("Invalid whole seconds in date string", 59);
    195 			if (input.hasNext()  &&  input.ch() != '.'  &&  input.ch() != 'Z'  &&
    196 				input.ch() != '+' && input.ch() != '-')
    197 			{
    198 				throw new XMPException("Invalid date string, after whole seconds",
    199 						XMPError.BADVALUE);
    200 			}
    201 			binValue.setSecond(value);
    202 			if (input.ch() == '.')
    203 			{
    204 				input.skip();
    205 				int digits = input.pos();
    206 				value = input.gatherInt("Invalid fractional seconds in date string", 999999999);
    207 				if (input.ch() != 'Z'  &&  input.ch() != '+'  &&  input.ch() != '-')
    208 				{
    209 					throw new XMPException("Invalid date string, after fractional second",
    210 							XMPError.BADVALUE);
    211 				}
    212 				digits = input.pos() - digits;
    213 				for (; digits > 9; --digits)
    214 				{
    215 					value = value / 10;
    216 				}
    217 				for (; digits < 9; ++digits)
    218 				{
    219 					value = value * 10;
    220 				}
    221 				binValue.setNanoSecond(value);
    222 			}
    223 		}
    224 
    225 		int tzSign = 0;
    226 		int tzHour = 0;
    227 		int tzMinute = 0;
    228 		if (input.ch() == 'Z')
    229 		{
    230 			input.skip();
    231 		}
    232 		else if (input.hasNext())
    233 		{
    234 			if (input.ch() == '+')
    235 			{
    236 				tzSign = 1;
    237 			}
    238 			else if (input.ch() == '-')
    239 			{
    240 				tzSign = -1;
    241 			}
    242 			else
    243 			{
    244 				throw new XMPException("Time zone must begin with 'Z', '+', or '-'",
    245 						XMPError.BADVALUE);
    246 			}
    247 
    248 			input.skip();
    249 			// Extract the time zone hour.
    250 			tzHour = input.gatherInt("Invalid time zone hour in date string", 23);
    251 			if (input.ch() != ':')
    252 			{
    253 				throw new XMPException("Invalid date string, after time zone hour",
    254 						XMPError.BADVALUE);
    255 			}
    256 			input.skip();
    257 
    258 			// Extract the time zone minute.
    259 			tzMinute = input.gatherInt("Invalid time zone minute in date string", 59);
    260 		}
    261 
    262 		// create a corresponding TZ and set it time zone
    263 		int offset = (tzHour * 3600 * 1000 + tzMinute * 60 * 1000) * tzSign;
    264 		binValue.setTimeZone(new SimpleTimeZone(offset, ""));
    265 
    266 
    267 		if (input.hasNext())
    268 		{
    269 			throw new XMPException(
    270 				"Invalid date string, extra chars at end", XMPError.BADVALUE);
    271 		}
    272 
    273 		return binValue;
    274 	}
    275 
    276 
    277 	/**
    278 	 * Converts a <code>Calendar</code> into an ISO 8601 string.
    279 	 * Format a date according to ISO 8601 and http://www.w3.org/TR/NOTE-datetime:
    280 	 * <ul>
    281 	 * <li>YYYY
    282 	 * <li>YYYY-MM
    283 	 * <li>YYYY-MM-DD
    284 	 * <li>YYYY-MM-DDThh:mmTZD
    285 	 * <li>YYYY-MM-DDThh:mm:ssTZD
    286 	 * <li>YYYY-MM-DDThh:mm:ss.sTZD
    287 	 * </ul>
    288 	 *
    289 	 * Data fields:
    290 	 * <ul>
    291 	 * <li>YYYY = four-digit year
    292 	 * <li>MM	 = two-digit month (01=January, etc.)
    293 	 * <li>DD	 = two-digit day of month (01 through 31)
    294 	 * <li>hh	 = two digits of hour (00 through 23)
    295 	 * <li>mm	 = two digits of minute (00 through 59)
    296 	 * <li>ss	 = two digits of second (00 through 59)
    297 	 * <li>s	 = one or more digits representing a decimal fraction of a second
    298 	 * <li>TZD	 = time zone designator (Z or +hh:mm or -hh:mm)
    299 	 * </ul>
    300 	 * <p>
    301 	 * <em>Note:</em> ISO 8601 does not seem to allow years less than 1000 or greater than 9999.
    302 	 * We allow any year, even negative ones. The year is formatted as "%.4d".<p>
    303 	 * <em>Note:</em> Fix for bug 1269463 (silently fix out of range values) included in parsing.
    304 	 * The quasi-bogus "time only" values from Photoshop CS are not supported.
    305 	 *
    306 	 * @param dateTime an XMPDateTime-object.
    307 	 * @return Returns an ISO 8601 string.
    308 	 */
    309 	public static String render(XMPDateTime dateTime)
    310 	{
    311 		StringBuffer buffer = new StringBuffer();
    312 
    313 		// year is rendered in any case, even 0000
    314 		DecimalFormat df = new DecimalFormat("0000", new DecimalFormatSymbols(Locale.ENGLISH));
    315 		buffer.append(df.format(dateTime.getYear()));
    316 		if (dateTime.getMonth() == 0)
    317 		{
    318 			return buffer.toString();
    319 		}
    320 
    321 		// month
    322 		df.applyPattern("'-'00");
    323 		buffer.append(df.format(dateTime.getMonth()));
    324 		if (dateTime.getDay() == 0)
    325 		{
    326 			return buffer.toString();
    327 		}
    328 
    329 		// day
    330 		buffer.append(df.format(dateTime.getDay()));
    331 
    332 		// time, rendered if any time field is not zero
    333 		if (dateTime.getHour() != 0  ||
    334 			dateTime.getMinute() != 0  ||
    335 			dateTime.getSecond() != 0  ||
    336 			dateTime.getNanoSecond() != 0  ||
    337 			(dateTime.getTimeZone() != null  &&  dateTime.getTimeZone().getRawOffset() != 0))
    338 		{
    339 			// hours and minutes
    340 			buffer.append('T');
    341 			df.applyPattern("00");
    342 			buffer.append(df.format(dateTime.getHour()));
    343 			buffer.append(':');
    344 			buffer.append(df.format(dateTime.getMinute()));
    345 
    346 			// seconds and nanoseconds
    347 			if (dateTime.getSecond() != 0 || dateTime.getNanoSecond() != 0)
    348 			{
    349 				double seconds = dateTime.getSecond() + dateTime.getNanoSecond() / 1e9d;
    350 
    351 				df.applyPattern(":00.#########");
    352 				buffer.append(df.format(seconds));
    353 			}
    354 
    355 			// time zone
    356 			if (dateTime.getTimeZone() != null)
    357 			{
    358 				// used to calculate the time zone offset incl. Daylight Savings
    359 				long timeInMillis = dateTime.getCalendar().getTimeInMillis();
    360 				int offset = dateTime.getTimeZone().getOffset(timeInMillis);
    361 				if (offset == 0)
    362 				{
    363 					// UTC
    364 					buffer.append('Z');
    365 				}
    366 				else
    367 				{
    368 					int thours = offset / 3600000;
    369 					int tminutes = Math.abs(offset % 3600000 / 60000);
    370 					df.applyPattern("+00;-00");
    371 					buffer.append(df.format(thours));
    372 					df.applyPattern(":00");
    373 					buffer.append(df.format(tminutes));
    374 				}
    375 			}
    376 		}
    377 		return buffer.toString();
    378 	}
    379 
    380 
    381 }
    382 
    383 
    384 /**
    385  * @since   22.08.2006
    386  */
    387 class ParseState
    388 {
    389 	/** */
    390 	private String str;
    391 	/** */
    392 	private int pos = 0;
    393 
    394 
    395 	/**
    396 	 * @param str initializes the parser container
    397 	 */
    398 	public ParseState(String str)
    399 	{
    400 		this.str = str;
    401 	}
    402 
    403 
    404 	/**
    405 	 * @return Returns the length of the input.
    406 	 */
    407 	public int length()
    408 	{
    409 		return str.length();
    410 	}
    411 
    412 
    413 	/**
    414 	 * @return Returns whether there are more chars to come.
    415 	 */
    416 	public boolean hasNext()
    417 	{
    418 		return pos < str.length();
    419 	}
    420 
    421 
    422 	/**
    423 	 * @param index index of char
    424 	 * @return Returns char at a certain index.
    425 	 */
    426 	public char ch(int index)
    427 	{
    428 		return index < str.length() ?
    429 			str.charAt(index) :
    430 			0x0000;
    431 	}
    432 
    433 
    434 	/**
    435 	 * @return Returns the current char or 0x0000 if there are no more chars.
    436 	 */
    437 	public char ch()
    438 	{
    439 		return pos < str.length() ?
    440 			str.charAt(pos) :
    441 			0x0000;
    442 	}
    443 
    444 
    445 	/**
    446 	 * Skips the next char.
    447 	 */
    448 	public void skip()
    449 	{
    450 		pos++;
    451 	}
    452 
    453 
    454 	/**
    455 	 * @return Returns the current position.
    456 	 */
    457 	public int pos()
    458 	{
    459 		return pos;
    460 	}
    461 
    462 
    463 	/**
    464 	 * Parses a integer from the source and sets the pointer after it.
    465 	 * @param errorMsg Error message to put in the exception if no number can be found
    466 	 * @param maxValue the max value of the number to return
    467 	 * @return Returns the parsed integer.
    468 	 * @throws XMPException Thrown if no integer can be found.
    469 	 */
    470 	public int gatherInt(String errorMsg, int maxValue) throws XMPException
    471 	{
    472 		int value = 0;
    473 		boolean success = false;
    474 		char ch = ch(pos);
    475 		while ('0' <= ch  &&  ch <= '9')
    476 		{
    477 			value = (value * 10) + (ch - '0');
    478 			success = true;
    479 			pos++;
    480 			ch = ch(pos);
    481 		}
    482 
    483 		if (success)
    484 		{
    485 			if (value > maxValue)
    486 			{
    487 				return maxValue;
    488 			}
    489 			else if (value < 0)
    490 			{
    491 				return 0;
    492 			}
    493 			else
    494 			{
    495 				return value;
    496 			}
    497 		}
    498 		else
    499 		{
    500 			throw new XMPException(errorMsg, XMPError.BADVALUE);
    501 		}
    502 	}
    503 }
    504 
    505 
    506