Index: test/java/org/apache/xmlgraphics/xmp/DateFormattingTest.java =================================================================== --- test/java/org/apache/xmlgraphics/xmp/DateFormattingTest.java (revision 1333394) +++ test/java/org/apache/xmlgraphics/xmp/DateFormattingTest.java (working copy) @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* $Id$ */ - -package org.apache.xmlgraphics.xmp; - -import java.util.Calendar; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -import junit.framework.TestCase; - -/** - * Tests date formatting for XMP. - */ -public class DateFormattingTest extends TestCase { - - /** - * Checks date formatting for XMP. - * @throws Exception if an error occurs - */ - public void testDateFormatting() throws Exception { - Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.ENGLISH); - cal.set(2008, Calendar.FEBRUARY, 07, 15, 11, 07); - cal.set(Calendar.MILLISECOND, 0); - Date dt = cal.getTime(); - - String s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT")); - assertEquals("2008-02-07T15:11:07Z", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); - - s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT+02:00")); - assertEquals("2008-02-07T17:11:07+02:00", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); - - s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT+02:30")); - assertEquals("2008-02-07T17:41:07+02:30", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); - - s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT-08:00")); - assertEquals("2008-02-07T07:11:07-08:00", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); - - s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT-11:00")); - assertEquals("2008-02-07T04:11:07-11:00", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); - } - -} Index: test/java/org/apache/xmlgraphics/util/DateFormatUtilTestCase.java =================================================================== --- test/java/org/apache/xmlgraphics/util/DateFormatUtilTestCase.java (revision 1333020) +++ test/java/org/apache/xmlgraphics/util/DateFormatUtilTestCase.java (working copy) @@ -17,49 +17,95 @@ /* $Id$ */ -package org.apache.xmlgraphics.xmp; +package org.apache.xmlgraphics.util; import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.TimeZone; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; + import junit.framework.TestCase; /** * Tests date formatting for XMP. */ -public class DateFormattingTest extends TestCase { +public class DateFormatUtilTestCase extends TestCase { /** * Checks date formatting for XMP. * @throws Exception if an error occurs */ - public void testDateFormatting() throws Exception { - Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.ENGLISH); - cal.set(2008, Calendar.FEBRUARY, 07, 15, 11, 07); - cal.set(Calendar.MILLISECOND, 0); - Date dt = cal.getTime(); + public void testDateFormattingISO8601() throws Exception { + Date dt = createTestDate(); String s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT")); assertEquals("2008-02-07T15:11:07Z", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT+02:00")); assertEquals("2008-02-07T17:11:07+02:00", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT+02:30")); assertEquals("2008-02-07T17:41:07+02:30", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT-08:00")); assertEquals("2008-02-07T07:11:07-08:00", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT-11:00")); assertEquals("2008-02-07T04:11:07-11:00", s); - assertEquals(dt, XMPSchemaAdapter.parseISO8601Date(s)); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); } + private Date createTestDate() { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.ENGLISH); + cal.set(2008, Calendar.FEBRUARY, 07, 15, 11, 07); + cal.set(Calendar.MILLISECOND, 0); + Date dt = cal.getTime(); + return dt; + } + + public void testDateFormattingPDF() throws Exception { + Date dt = createTestDate(); + + String s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT")); + assertEquals("D:20080207151107Z", s); + + s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT+02:00")); + assertEquals("D:20080207171107+02'00'", s); + + s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT+02:30")); + assertEquals("D:20080207174107+02'30'", s); + + s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT-08:00")); + assertEquals("D:20080207071107-08'00'", s); + + s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT-11:00")); + assertEquals("D:20080207041107-11'00'", s); + } + + public void testParseInvalidDateNoColonUTC() { + testInvalidDate("2008-02-07T151107Z"); + } + + public void testParseInvalidDateNoColonLocal() { + testInvalidDate("2008-02-07T151107+0000"); + } + + public void testParseInvalidDateColonLast() { + testInvalidDate("2008-02-07T151107Z:"); + } + + private void testInvalidDate(String date) { + try { + DateFormatUtil.parseISO8601Date(date); + fail(); + } catch (IllegalArgumentException e) { + // Expected + } + } } Index: src/java/org/apache/xmlgraphics/xmp/XMPSchemaAdapter.java =================================================================== --- src/java/org/apache/xmlgraphics/xmp/XMPSchemaAdapter.java (revision 1333394) +++ src/java/org/apache/xmlgraphics/xmp/XMPSchemaAdapter.java (working copy) @@ -19,14 +19,10 @@ package org.apache.xmlgraphics.xmp; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Calendar; import java.util.Date; -import java.util.Locale; import java.util.TimeZone; +import org.apache.xmlgraphics.util.DateFormatUtil; import org.apache.xmlgraphics.util.QName; /** @@ -160,13 +156,6 @@ return formatISO8601Date(dt, TimeZone.getDefault()); } - private static DateFormat createPseudoISO8601DateFormat() { - DateFormat df = new SimpleDateFormat( - "yyyy'-'MM'-'dd'T'HH':'mm':'ss", Locale.ENGLISH); - df.setTimeZone(TimeZone.getTimeZone("GMT")); - return df; - } - /** * Formats a Date using ISO 8601 format in the given time zone. * @param dt the date @@ -174,82 +163,10 @@ * @return the formatted date */ public static String formatISO8601Date(Date dt, TimeZone tz) { - //ISO 8601 cannot be expressed directly using SimpleDateFormat - Calendar cal = Calendar.getInstance(tz, Locale.ENGLISH); - cal.setTime(dt); - int offset = cal.get(Calendar.ZONE_OFFSET); - offset += cal.get(Calendar.DST_OFFSET); - - //DateFormat is operating on GMT so adjust for time zone offset - Date dt1 = new Date(dt.getTime() + offset); - StringBuffer sb = new StringBuffer(createPseudoISO8601DateFormat().format(dt1)); - - offset /= (1000 * 60); //Convert to minutes - - if (offset == 0) { - sb.append('Z'); - } else { - int zoneOffsetHours = offset / 60; - int zoneOffsetMinutes = Math.abs(offset % 60); - if (zoneOffsetHours > 0) { - sb.append('+'); - } else { - sb.append('-'); - } - if (Math.abs(zoneOffsetHours) < 10) { - sb.append('0'); - } - sb.append(Math.abs(zoneOffsetHours)); - sb.append(':'); - if (zoneOffsetMinutes < 10) { - sb.append('0'); - } - sb.append(zoneOffsetMinutes); - } - - return sb.toString(); + return DateFormatUtil.formatISO8601(dt, tz); } /** - * Parses an ISO 8601 date and time value. - * @param dt the date and time value as an ISO 8601 string - * @return the parsed date/time - */ - public static Date parseISO8601Date(final String dt) { - //TODO Parse formats other than yyyy-mm-ddThh:mm:ssZ - int offset = 0; - String parsablePart; - if (dt.endsWith("Z")) { - parsablePart = dt.substring(0, dt.length() - 1); - } else { - int pos; - int neg = 1; - pos = dt.lastIndexOf('+'); - if (pos < 0) { - pos = dt.lastIndexOf('-'); - neg = -1; - } - if (pos >= 0) { - String timeZonePart = dt.substring(pos); - parsablePart = dt.substring(0, pos); - offset = Integer.parseInt(timeZonePart.substring(1, 3)) * 60; - offset += Integer.parseInt(timeZonePart.substring(4, 6)); - offset *= neg; - } else { - parsablePart = dt; - } - } - Date d; - try { - d = createPseudoISO8601DateFormat().parse(parsablePart); - } catch (ParseException e) { - throw new IllegalArgumentException("Invalid ISO 8601 date format: " + dt); - } - d.setTime(d.getTime() - offset * 60 * 1000); - return d; - } - - /** * Adds a date value to an ordered array. * @param propName the property name * @param value the date value @@ -279,7 +196,7 @@ if (dt == null) { return null; } else { - return parseISO8601Date(dt); + return DateFormatUtil.parseISO8601Date(dt); } } @@ -514,9 +431,9 @@ for (int i = 0, c = res.length; i < c; i++) { Object obj = arr[i]; if (obj instanceof Date) { - res[i] = (Date)((Date)obj).clone(); + res[i] = (Date) ((Date) obj).clone(); } else { - res[i] = parseISO8601Date(obj.toString()); + res[i] = DateFormatUtil.parseISO8601Date(obj.toString()); } } return res; Index: src/java/org/apache/xmlgraphics/util/DateFormatUtil.java =================================================================== --- src/java/org/apache/xmlgraphics/util/DateFormatUtil.java (revision 0) +++ src/java/org/apache/xmlgraphics/util/DateFormatUtil.java (revision 0) @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.xmlgraphics.util; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public final class DateFormatUtil { + + private static final String ISO_8601_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + + private DateFormatUtil() { + } + + /** + * Formats the date according to PDF format. See section 3.8.2 of the PDF 1.4 specification. + * @param date The date time to format + * @param timeZone The time zone used to format the date + * @return a formatted date according to PDF format (based on ISO 8824) + */ + public static String formatPDFDate(Date date, TimeZone timeZone) { + DateFormat dateFormat = createDateFormat("'D:'yyyyMMddHHmmss", timeZone); + return formatDate(date, dateFormat, '\'', true); + } + + /** + * Formats the date according to ISO 8601 standard. + * @param date The date time to format + * @param timeZone The time zone used to format the date + * @return a formatted date according to ISO 8601 + */ + public static String formatISO8601(Date date, TimeZone timeZone) { + DateFormat dateFormat = createDateFormat(ISO_8601_DATE_PATTERN, timeZone); + return formatDate(date, dateFormat, ':', false); + } + + private static DateFormat createDateFormat(String format, TimeZone timeZone) { + DateFormat dateFormat = new SimpleDateFormat(format, Locale.ENGLISH); + dateFormat.setTimeZone(timeZone); + return dateFormat; + } + + /** + * Formats the date according to the specified format and returns as a string. + * @param date The date / time object to format + * @param dateFormat The date format to use when outputting the date + * @param delimiter The character used to separate the time zone difference hours and minutes + * @param endWithDelimiter Determines whether the date string will end with the delimiter character + * @return the formatted date string + */ + private static String formatDate(Date date, DateFormat dateFormat, char delimiter, + boolean endWithDelimiter) { + Calendar cal = Calendar.getInstance(dateFormat.getTimeZone(), Locale.ENGLISH); + cal.setTime(date); + int offset = getOffsetInMinutes(cal); + StringBuilder sb = new StringBuilder(dateFormat.format(date)); + appendOffset(sb, delimiter, offset, endWithDelimiter); + return sb.toString(); + } + + private static int getOffsetInMinutes(Calendar cal) { + int offset = cal.get(Calendar.ZONE_OFFSET); + offset += cal.get(Calendar.DST_OFFSET); + offset /= (1000 * 60); + return offset; + } + + private static void appendOffset(StringBuilder sb, char delimiter, int offset, boolean endWithDelimiter) { + if (offset == 0) { + appendOffsetUTC(sb); + } else { + appendOffsetNoUTC(sb, delimiter, offset, endWithDelimiter); + } + } + + private static void appendOffsetUTC(StringBuilder sb) { + sb.append('Z'); + } + + private static void appendOffsetNoUTC(StringBuilder sb, char delimiter, int offset, + boolean endWithDelimiter) { + int zoneOffsetHours = offset / 60; + appendOffsetSign(sb, zoneOffsetHours); + appendPaddedNumber(sb, Math.abs(zoneOffsetHours)); + sb.append(delimiter); + appendPaddedNumber(sb, Math.abs(offset % 60)); + if (endWithDelimiter) { + sb.append(delimiter); + } + } + + private static void appendOffsetSign(StringBuilder sb, int zoneOffsetHours) { + if (zoneOffsetHours >= 0) { + sb.append('+'); + } else { + sb.append('-'); + } + } + + private static void appendPaddedNumber(StringBuilder sb, int number) { + if (number < 10) { + sb.append('0'); + } + sb.append(number); + } + + /** + * Parses an ISO 8601 date and time value. + * @param date the date and time value as an ISO 8601 string + * @return the parsed date/time + */ + public static Date parseISO8601Date(String date) { + final String errorMessage = "Invalid ISO 8601 date format: "; + date = formatDateToParse(date, errorMessage); + DateFormat dateFormat = new SimpleDateFormat(ISO_8601_DATE_PATTERN + "Z"); + try { + return dateFormat.parse(date); + } catch (ParseException ex) { + throw new IllegalArgumentException(errorMessage + date); + } + } + + private static String formatDateToParse(String date, String errorMessage) { + /* Remove the colon from the time zone difference (+08:00) so that it can be parsed + * by the SimpleDateFormat string. + */ + if (!date.contains("Z")) { + int lastColonIndex = date.lastIndexOf(":"); + if (lastColonIndex < 0) { + throw new IllegalArgumentException(errorMessage + date); + } + date = date.substring(0, lastColonIndex) + date.substring(lastColonIndex + 1, date.length()); + } else { + date = date.replace("Z", "+0000"); + } + return date; + } + +}