• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
ISO8601Converter()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 	 */
parse(String iso8601String)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 	 */
parse(String iso8601String, XMPDateTime binValue)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 	 */
render(XMPDateTime dateTime)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 	 */
ParseState(String str)398 	public ParseState(String str)
399 	{
400 		this.str = str;
401 	}
402 
403 
404 	/**
405 	 * @return Returns the length of the input.
406 	 */
length()407 	public int length()
408 	{
409 		return str.length();
410 	}
411 
412 
413 	/**
414 	 * @return Returns whether there are more chars to come.
415 	 */
hasNext()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 	 */
ch(int index)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 	 */
ch()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 	 */
skip()448 	public void skip()
449 	{
450 		pos++;
451 	}
452 
453 
454 	/**
455 	 * @return Returns the current position.
456 	 */
pos()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 	 */
gatherInt(String errorMsg, int maxValue)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