From 680a4833e20c7bb9789b4fac62e8a7410af3e26b Mon Sep 17 00:00:00 2001 From: gmantele <gmantele@ari.uni-heidelberg.de> Date: Wed, 1 Oct 2014 16:43:43 +0200 Subject: [PATCH] [UWS,TAP] Fix timestamp format. Now, each returned date will expressed in UTC and using the ISO8601 format. Any given date is also expected in UTC (or with a time zone offset) and in ISO8601Format. These rules apply also for uploaded tables and for timestamp columns in a query result. --- src/org/json/Json4Uws.java | 16 +- src/tap/data/ResultSetTableIterator.java | 8 +- src/tap/db/JDBCConnection.java | 19 +- .../TAPDestructionTimeController.java | 7 +- src/uws/ISO8601Format.java | 342 ++++++++++++++++++ src/uws/job/UWSJob.java | 12 +- .../parameters/DestructionTimeController.java | 10 +- src/uws/job/parameters/UWSParameters.java | 3 +- src/uws/job/serializer/JSONSerializer.java | 9 +- src/uws/job/serializer/UWSSerializer.java | 11 +- src/uws/job/serializer/XMLSerializer.java | 14 +- .../backup/DefaultUWSBackupManager.java | 9 +- test/uws/TestISO8601Format.java | 163 +++++++++ 13 files changed, 582 insertions(+), 41 deletions(-) create mode 100644 src/uws/ISO8601Format.java create mode 100644 test/uws/TestISO8601Format.java diff --git a/src/org/json/Json4Uws.java b/src/org/json/Json4Uws.java index bfa25e3..4131137 100644 --- a/src/org/json/Json4Uws.java +++ b/src/org/json/Json4Uws.java @@ -16,26 +16,26 @@ package org.json; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.util.Iterator; +import uws.ISO8601Format; import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; - import uws.job.user.JobOwner; - import uws.service.UWS; import uws.service.UWSUrl; /** * Useful conversion functions from UWS to JSON. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) */ public final class Json4Uws { @@ -130,11 +130,11 @@ public final class Json4Uws { json.put(UWSJob.PARAM_OWNER, job.getOwner().getPseudo()); json.put(UWSJob.PARAM_QUOTE, job.getQuote()); if (job.getStartTime() != null) - json.put(UWSJob.PARAM_START_TIME, UWSJob.dateFormat.format(job.getStartTime())); + json.put(UWSJob.PARAM_START_TIME, ISO8601Format.format(job.getStartTime())); if (job.getEndTime() != null) - json.put(UWSJob.PARAM_END_TIME, UWSJob.dateFormat.format(job.getEndTime())); + json.put(UWSJob.PARAM_END_TIME, ISO8601Format.format(job.getEndTime())); if (job.getDestructionTime() != null) - json.put(UWSJob.PARAM_DESTRUCTION_TIME, UWSJob.dateFormat.format(job.getDestructionTime())); + json.put(UWSJob.PARAM_DESTRUCTION_TIME, ISO8601Format.format(job.getDestructionTime())); json.put(UWSJob.PARAM_EXECUTION_DURATION, job.getExecutionDuration()); json.put(UWSJob.PARAM_PARAMETERS, getJobParamsJson(job)); json.put(UWSJob.PARAM_RESULTS, getJobResultsJson(job)); diff --git a/src/tap/data/ResultSetTableIterator.java b/src/tap/data/ResultSetTableIterator.java index d632e72..6f9f6d7 100644 --- a/src/tap/data/ResultSetTableIterator.java +++ b/src/tap/data/ResultSetTableIterator.java @@ -22,11 +22,13 @@ package tap.data; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.NoSuchElementException; import tap.metadata.TAPColumn; import tap.metadata.TAPType; import tap.metadata.TAPType.TAPDatatype; +import uws.ISO8601Format; import adql.db.DBColumn; /** @@ -271,7 +273,11 @@ public class ResultSetTableIterator implements TableIterator { // Get the column value: try{ - return data.getObject(++colIndex); + Object o = data.getObject(++colIndex); + // if the column value is a Timestamp object, format it in ISO8601: + if (o != null && o instanceof Timestamp) + o = ISO8601Format.format(((Timestamp)o).getTime()); + return o; }catch(SQLException se){ throw new DataReadException("Can not read the value of the " + colIndex + "-th column!", se); } diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java index 72e87d4..fc1331f 100644 --- a/src/tap/db/JDBCConnection.java +++ b/src/tap/db/JDBCConnection.java @@ -27,6 +27,8 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Timestamp; +import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -47,6 +49,7 @@ import tap.metadata.TAPTable; import tap.metadata.TAPTable.TableType; import tap.metadata.TAPType; import tap.metadata.TAPType.TAPDatatype; +import uws.ISO8601Format; import uws.service.log.UWSLog.LogLevel; import adql.query.ADQLQuery; import adql.query.IdentifierField; @@ -1584,8 +1587,20 @@ public class JDBCConnection implements DBConnection { while(data.nextRow()){ nbRows++; int c = 1; - while(data.hasNextCol()) - stmt.setObject(c++, data.nextCol()); + while(data.hasNextCol()){ + Object val = data.nextCol(); + /* If the value is supposed to be a Timestamp, parse it + * and build an appropriate SQL object: */ + if (val != null && cols[c - 1].getDatatype().type == TAPDatatype.TIMESTAMP){ + try{ + val = new Timestamp(ISO8601Format.parse(val.toString())); + }catch(ParseException pe){ + logger.logDB(LogLevel.ERROR, this, "UPLOAD", "Unexpected date format for the " + c + "-th column (" + val + ")! A date formatted in ISO8601 was expected.", pe); + throw new DBException("Unexpected date format for the " + c + "-th column (" + val + ")! A date formatted in ISO8601 was expected.", pe); + } + } + stmt.setObject(c++, val); + } executeUpdate(stmt, nbRows); } executeBatchUpdates(stmt, nbRows); diff --git a/src/tap/parameters/TAPDestructionTimeController.java b/src/tap/parameters/TAPDestructionTimeController.java index 4546561..0c3a403 100644 --- a/src/tap/parameters/TAPDestructionTimeController.java +++ b/src/tap/parameters/TAPDestructionTimeController.java @@ -25,6 +25,7 @@ import java.util.Calendar; import java.util.Date; import tap.ServiceConnection; +import uws.ISO8601Format; import uws.UWSException; import uws.job.UWSJob; import uws.job.parameters.DestructionTimeController.DateField; @@ -146,12 +147,12 @@ public class TAPDestructionTimeController implements InputParamController { else if (value instanceof String){ String strValue = (String)value; try{ - date = UWSJob.dateFormat.parse(strValue); + date = ISO8601Format.parseToDate(strValue); }catch(ParseException pe){ - throw new UWSException(UWSException.BAD_REQUEST, pe, "Wrong date format for the parameter \"destruction\": \"" + strValue + "\"! The format to respect is: " + UWSJob.DEFAULT_DATE_FORMAT); + throw new UWSException(UWSException.BAD_REQUEST, pe, "Wrong date format for the parameter \"destruction\": \"" + strValue + "\"! A date must be formatted in the ISO8601 format (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional)."); } }else - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"destruction\": class \"" + value.getClass().getName() + "\"! It should be a Date or a string containing a date with the format \"" + UWSJob.DEFAULT_DATE_FORMAT + "\"."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"destruction\": class \"" + value.getClass().getName() + "\"! It should be a Date or a string containing a date formatted in ISO8601 (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional)."); Date maxDate = getMaxDestructionTime(); if (maxDate != null && date.after(maxDate)) diff --git a/src/uws/ISO8601Format.java b/src/uws/ISO8601Format.java new file mode 100644 index 0000000..61fa2d5 --- /dev/null +++ b/src/uws/ISO8601Format.java @@ -0,0 +1,342 @@ +package uws; + +import java.text.DecimalFormat; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * <p>Let formatting and parsing date expressed in ISO8601 format.</p> + * + * <h3>Date formatting</h3> + * + * <p> + * Dates are formatted using the following format: "yyyy-MM-dd'T'hh:mm:ss'Z'" if in UTC or "yyyy-MM-dd'T'hh:mm:ss[+|-]hh:mm" otherwise. + * On the contrary to the time zone, by default the number of milliseconds is not displayed. However, when displayed, the format is: + * "yyyy-MM-dd'T'hh:mm:ss.sss'Z'" if in UTC or "yyyy-MM-dd'T'hh:mm:ss.sss[+|-]hh:mm" otherwise. + * </b> + * + * <p> + * As said previously, it is possible to display or to hide the time zone and the milliseconds. This can be easily done by changing + * the value of the static attributes {@link #displayTimeZone} and {@link #displayMilliseconds}. By default {@link #displayTimeZone} is <i>true</i> + * and {@value #displayMilliseconds} is <i>false</i>. + * </i> + * + * <p> + * By default the date will be formatted in the local time zone. But this could be specified either in the format function {@link #format(long, String, boolean, boolean)} + * or by changing the static attribute {@link #targetTimeZone}. The time zone must be specified with its ID. The list of all available time zone IDs is given by + * {@link TimeZone#getAvailableIDs()}. + * </p> + * + * <h3>Date parsing</h3> + * + * <p> + * This class is able to parse dates - with the function {@link #parse(String)} - formatted strictly in ISO8601 + * but is also more permissive. Particularly, separators (like '-' and ':') are optional. The date and time separator + * ('T') can be replaced by a space. + * </p> + * + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (10/2014) + * @since 4.1 + */ +public class ISO8601Format { + + /** Indicate whether any date formatted with this class displays the time zone. */ + public static boolean displayTimeZone = false; + /** Indicate whether any date formatted with this class displays the milliseconds. */ + public static boolean displayMilliseconds = false; + /** Indicate the time zone in which the date and time should be formatted (whatever is the time zone of the given date). */ + public static String targetTimeZone = "UTC"; // for the local time zone: TimeZone.getDefault().getID(); + + /** Object to use to format numbers with two digits (ie. 12, 02, 00). */ + protected final static DecimalFormat twoDigitsFmt = new DecimalFormat("00"); + /** Object to use to format numbers with three digits (ie. 001, 000, 123). */ + protected final static DecimalFormat threeDigitsFmt = new DecimalFormat("000"); + + /** + * <p>Format the given date-time in ISO8601 format.</p> + * + * <p><i>Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, ISO8601Format.targetTimeZone, ISO8601Format.displayTimeZone, ISO8601Format.displayMilliseconds. + * </i></p> + * + * @param date Date-time. + * + * @return Date formatted in ISO8601. + */ + public static String format(final Date date){ + return format(date.getTime(), targetTimeZone, displayTimeZone, displayMilliseconds); + } + + /** + * <p>Format the given date-time in ISO8601 format.</p> + * + * <p><i>Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, ISO8601Format.targetTimeZone, ISO8601Format.displayTimeZone, ISO8601Format.displayMilliseconds. + * </i></p> + * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * + * @return Date formatted in ISO8601. + */ + public static String format(final long date){ + return format(date, targetTimeZone, displayTimeZone, displayMilliseconds); + } + + /** + * <p>Convert the given date-time in the given time zone and format it in ISO8601 format.</p> + * + * <p><i>Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, ISO8601Format.targetTimeZone, withTimeZone, ISO8601Format.displayMilliseconds. + * </i></p> + * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * @param withTimeZone Target time zone. + * + * @return Date formatted in ISO8601. + */ + public static String format(final long date, final boolean withTimeZone){ + return format(date, targetTimeZone, withTimeZone, displayMilliseconds); + } + + /** + * <p>Convert the given date-time in UTC and format it in ISO8601 format.</p> + * + * <p><i>Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, "UTC", ISO8601Format.displayTimeZone, ISO8601Format.displayMilliseconds. + * </i></p> + * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * + * @return Date formatted in ISO8601. + */ + public static String formatInUTC(final long date){ + return format(date, "UTC", displayTimeZone, displayMilliseconds); + } + + /** + * <p>Convert the given date-time in UTC and format it in ISO8601 format.</p> + * + * <p><i>Note: + * This function is equivalent to {@link #format(long, String, boolean, boolean)} with the following parameters: + * d, "UTC", withTimeZone, ISO8601Format.displayMilliseconds. + * </i></p> + * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * @param withTimeZone Target time zone. + * + * @return Date formatted in ISO8601. + */ + public static String formatInUTC(final long date, final boolean withTimeZone){ + return format(date, "UTC", withTimeZone, displayMilliseconds); + } + + /** + * Convert the given date in the given time zone and format it in ISO8601 format, with or without displaying the time zone + * and/or the milliseconds field. + * + * @param date Date-time in milliseconds (from the 1st January 1970 ; this value is returned by java.util.Date#getTime()). + * @param targetTimeZone Target time zone. + * @param withTimeZone <i>true</i> to display the time zone, <i>false</i> otherwise. + * @param withMillisec <i>true</i> to display the milliseconds, <i>false</i> otherwise. + * + * @return Date formatted in ISO8601. + */ + protected static String format(final long date, final String targetTimeZone, final boolean withTimeZone, final boolean withMillisec){ + GregorianCalendar cal = new GregorianCalendar(); + cal.setTimeInMillis(date); + + // Convert the given date in the target Time Zone: + if (targetTimeZone != null && targetTimeZone.length() > 0) + cal.setTimeZone(TimeZone.getTimeZone(targetTimeZone)); + else + cal.setTimeZone(TimeZone.getTimeZone(ISO8601Format.targetTimeZone)); + + StringBuffer buf = new StringBuffer(); + + // Date with format yyyy-MM-dd : + buf.append(cal.get(Calendar.YEAR)).append('-'); + buf.append(twoDigitsFmt.format(cal.get(Calendar.MONTH) + 1)).append('-'); + buf.append(twoDigitsFmt.format(cal.get(Calendar.DAY_OF_MONTH))); + + // Time with format 'T'HH:mm:ss : + buf.append('T').append(twoDigitsFmt.format(cal.get(Calendar.HOUR_OF_DAY))).append(':'); + buf.append(twoDigitsFmt.format(cal.get(Calendar.MINUTE))).append(':'); + buf.append(twoDigitsFmt.format(cal.get(Calendar.SECOND))); + if (withMillisec){ + buf.append('.').append(threeDigitsFmt.format(cal.get(Calendar.MILLISECOND))); + } + + // Time zone with format (+|-)HH:mm : + if (withTimeZone){ + int tzOffset = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / (60 * 1000); // offset in minutes + boolean negative = (tzOffset < 0); + if (negative) + tzOffset *= -1; + int hours = tzOffset / 60, minutes = tzOffset - (hours * 60); + if (hours == 0 && minutes == 0) + buf.append('Z'); + else{ + buf.append(negative ? '-' : '+'); + buf.append(twoDigitsFmt.format(hours)).append(':'); + buf.append(twoDigitsFmt.format(minutes)); + } + } + + return buf.toString(); + } + + /** + * <p>Parse the given date expressed using the ISO8601 format ("yyyy-MM-dd'T'hh:mm:ss.sssZ" + * or "yyyy-MM-dd'T'hh:mm:ss.sssZ[+|-]hh:mm:ss").</p> + * + * <p> + * The syntax of the given date may be more or less strict. Particularly, separators like '-' and ':' are optional. + * Besides the date and time separator ('T') may be replaced by a space. + * </p> + * + * <p> + * The minimum allowed string is the date: "yyyy-MM-dd". All other date-time fields are optional, + * BUT, the time zone can be given without the time. + * </p> + * + * <p> + * If no time zone is specified (by a 'Z' or a time offset), the time zone in which the date is expressed + * is supposed to be the local one. + * </p> + * + * <p><i>Note: + * This function is equivalent to {@link #parse(String)}, but whose the returned value is used to create a Date object, like this: + * return new Date(parse(strDate)). + * </i></p> + * + * @param strDate Date expressed as a string in ISO8601 format. + * + * @return Parsed date (expressed in milliseconds from the 1st January 1970 ; + * a date can be easily built with this number using {@link java.util.Date#Date(long)}). + * + * @throws ParseException If the given date is not expressed in ISO8601 format or is not merely parseable with this implementation. + */ + public final static Date parseToDate(final String strDate) throws ParseException{ + return new Date(parse(strDate)); + } + + /** + * <p>Parse the given date expressed using the ISO8601 format ("yyyy-MM-dd'T'hh:mm:ss.sssZ" + * or "yyyy-MM-dd'T'hh:mm:ss.sssZ[+|-]hh:mm:ss").</p> + * + * <p> + * The syntax of the given date may be more or less strict. Particularly, separators like '-' and ':' are optional. + * Besides the date and time separator ('T') may be replaced by a space. + * </p> + * + * <p> + * The minimum allowed string is the date: "yyyy-MM-dd". All other date-time fields are optional, + * BUT, the time zone can be given without the time. + * </p> + * + * <p> + * If no time zone is specified (by a 'Z' or a time offset), the time zone in which the date is expressed + * is supposed to be the local one. + * </p> + * + * @param strDate Date expressed as a string in ISO8601 format. + * + * @return Parsed date (expressed in milliseconds from the 1st January 1970 ; + * a date can be easily built with this number using {@link java.util.Date#Date(long)}). + * + * @throws ParseException If the given date is not expressed in ISO8601 format or is not merely parseable with this implementation. + */ + public static long parse(final String strDate) throws ParseException{ + Pattern p = Pattern.compile("(\\d{4})-?(\\d{2})-?(\\d{2})([T| ](\\d{2}):?(\\d{2}):?(\\d{2})(\\.(\\d+))?(Z|([\\+|\\-])(\\d{2}):?(\\d{2})(:?(\\d{2}))?)?)?"); + /* + * With this regular expression, we will get the following groups: + * + * ( 0: everything) + * 1: year (yyyy) + * 2: month (MM) + * 3: day (dd) + * ( 4: the full time part) + * 5: hours (hh) + * 6: minutes (mm) + * 7: seconds (ss) + * ( 8: the full ms part) + * 9: milliseconds (sss) + * (10: the full time zone part: 'Z' or the applied time offset) + * 11: sign of the offset ('+' if an addition was applied, '-' if it was a subtraction) + * 12: applied hours offset (hh) + * 13: applied minutes offset (mm) + * (14: the full seconds offset) + * 15: applied seconds offset (ss) + * + * Groups in parenthesis should be ignored ; but an exception must be done for the 10th which may contain 'Z' meaning a UTC time zone. + * + * All groups from the 4th (included) are optional. If not filled, an optional group is set to NULL. + * + * This regular expression is more permissive than the strict definition of the ISO8601 format. Particularly, separator characters + * ('-', 'T' and ':') are optional and it is possible to specify seconds in the time zone offset. + */ + + Matcher m = p.matcher(strDate); + if (m.matches()){ + Calendar cal = new GregorianCalendar(); + + // Set the time zone: + /* + * Note: In this library, we suppose that any date provided without specified time zone, is in UTC. + * + * It is more a TAP specification than a UWS one ; see the REC-TAP 1.0 at section 2.3.4 (page 15): + * "Within the ADQL query, the service must support the use of timestamp values in + * ISO8601 format, specifically yyyy-MM-dd['T'HH:mm:ss[.SSS]], where square + * brackets denote optional parts and the 'T' denotes a single character separator + * (T) between the date and time parts." + * + * ...and 2.5 (page 20): + * "TIMESTAMP values are specified using ISO8601 format without a timezone (as in 2.3.4 ) and are assumed to be in UTC." + */ + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + + // Set the date: + cal.set(Calendar.DAY_OF_MONTH, twoDigitsFmt.parse(m.group(3)).intValue()); + cal.set(Calendar.MONTH, twoDigitsFmt.parse(m.group(2)).intValue() - 1); + cal.set(Calendar.YEAR, Integer.parseInt(m.group(1))); + + // Set the time: + if (m.group(4) != null){ + cal.set(Calendar.HOUR_OF_DAY, twoDigitsFmt.parse(m.group(5)).intValue()); + cal.set(Calendar.MINUTE, twoDigitsFmt.parse(m.group(6)).intValue()); + cal.set(Calendar.SECOND, twoDigitsFmt.parse(m.group(7)).intValue()); + if (m.group(9) != null) + cal.set(Calendar.MILLISECOND, twoDigitsFmt.parse(m.group(9)).intValue()); + else + cal.set(Calendar.MILLISECOND, 0); + }else{ + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + } + + // Compute and apply the offset: + if (m.group(10) != null && !m.group(10).equals("Z")){ + int sign = (m.group(11).equals("-") ? 1 : -1); + cal.add(Calendar.HOUR_OF_DAY, sign * twoDigitsFmt.parse(m.group(12)).intValue()); + cal.add(Calendar.MINUTE, sign * twoDigitsFmt.parse(m.group(13)).intValue()); + if (m.group(15) != null) + cal.add(Calendar.SECOND, sign * twoDigitsFmt.parse(m.group(15)).intValue()); + } + + return cal.getTimeInMillis(); + }else + throw new ParseException("Invalid date format: \"" + strDate + "\"! An ISO8601 date was expected.", 0); + } +} diff --git a/src/uws/job/UWSJob.java b/src/uws/job/UWSJob.java index c26bad8..a373dba 100644 --- a/src/uws/job/UWSJob.java +++ b/src/uws/job/UWSJob.java @@ -22,6 +22,9 @@ package uws.job; import java.io.IOException; import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; @@ -33,6 +36,7 @@ import java.util.Vector; import javax.servlet.ServletOutputStream; +import uws.ISO8601Format; import uws.UWSException; import uws.UWSExceptionFactory; import uws.UWSToolBox; @@ -177,7 +181,9 @@ public class UWSJob extends SerializableUWSObject { /** Default value of {@link #owner} if no ID are given at the job creation. */ public final static String ANONYMOUS_OWNER = "anonymous"; - /** Default date format pattern. */ + /** Default date format pattern. + * @deprecated Replaced by {@link ISO8601Format}.*/ + @Deprecated public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; /** The quote value that indicates the quote of this job is not known. */ @@ -222,7 +228,9 @@ public class UWSJob extends SerializableUWSObject { */ private JobPhase phase; - /** The used date formatter. */ + /** The used date formatter. + * @deprecated Replaced by {@link ISO8601Format}. */ + @Deprecated public static final DateFormat dateFormat = new SimpleDateFormat(DEFAULT_DATE_FORMAT); /** diff --git a/src/uws/job/parameters/DestructionTimeController.java b/src/uws/job/parameters/DestructionTimeController.java index 011e7d2..35c9443 100644 --- a/src/uws/job/parameters/DestructionTimeController.java +++ b/src/uws/job/parameters/DestructionTimeController.java @@ -25,8 +25,8 @@ import java.text.ParseException; import java.util.Calendar; import java.util.Date; +import uws.ISO8601Format; import uws.UWSException; -import uws.job.UWSJob; /** * <p> @@ -46,7 +46,7 @@ import uws.job.UWSJob; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (08/2014) + * @version 4.1 (09/2014) */ public class DestructionTimeController implements InputParamController, Serializable { private static final long serialVersionUID = 1L; @@ -104,12 +104,12 @@ public class DestructionTimeController implements InputParamController, Serializ else if (value instanceof String){ String strValue = (String)value; try{ - date = UWSJob.dateFormat.parse(strValue); + date = ISO8601Format.parseToDate(strValue); }catch(ParseException pe){ - throw new UWSException(UWSException.BAD_REQUEST, pe, "Wrong date format for the destruction time parameter: \"" + strValue + "\"! The format to respect is: " + UWSJob.DEFAULT_DATE_FORMAT); + throw new UWSException(UWSException.BAD_REQUEST, pe, "Wrong date format for the destruction time parameter: \"" + strValue + "\"! Dates must be formatted in ISO8601 (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional)."); } }else - throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the destruction time parameter: class \"" + value.getClass().getName() + "\"! It should be a Date or a string containing a date with the format \"" + UWSJob.DEFAULT_DATE_FORMAT + "\"."); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the destruction time parameter: class \"" + value.getClass().getName() + "\"! It should be a Date or a string containing a date formatted in IS8601 (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional)."); Date maxDate = getMaxDestructionTime(); if (maxDate != null && date.after(maxDate)) diff --git a/src/uws/job/parameters/UWSParameters.java b/src/uws/job/parameters/UWSParameters.java index f33bdcc..d41afda 100644 --- a/src/uws/job/parameters/UWSParameters.java +++ b/src/uws/job/parameters/UWSParameters.java @@ -32,6 +32,7 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; +import uws.ISO8601Format; import uws.UWSException; import uws.job.UWSJob; import uws.service.UWS; @@ -588,7 +589,7 @@ public class UWSParameters implements Iterable<Entry<String,Object>> { return (Date)value; else if (value instanceof String){ try{ - Date destruction = UWSJob.dateFormat.parse((String)value); + Date destruction = ISO8601Format.parseToDate((String)value); synchronized(params){ params.put(UWSJob.PARAM_DESTRUCTION_TIME, destruction); } diff --git a/src/uws/job/serializer/JSONSerializer.java b/src/uws/job/serializer/JSONSerializer.java index 55fb337..48dc5db 100644 --- a/src/uws/job/serializer/JSONSerializer.java +++ b/src/uws/job/serializer/JSONSerializer.java @@ -23,6 +23,7 @@ package uws.job.serializer; import org.json.JSONException; import org.json.Json4Uws; +import uws.ISO8601Format; import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; @@ -35,7 +36,7 @@ import uws.service.UWSUrl; * Lets serializing any UWS resource in JSON. * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (08/2014) + * @version 4.1 (09/2014) * * @see Json4Uws */ @@ -103,7 +104,7 @@ public class JSONSerializer extends UWSSerializer { @Override public String getDestructionTime(final UWSJob job, final boolean root) throws JSONException{ if (job.getDestructionTime() != null){ - return Json4Uws.getJson(UWSJob.PARAM_DESTRUCTION_TIME, UWSJob.dateFormat.format(job.getDestructionTime())).toString(); + return Json4Uws.getJson(UWSJob.PARAM_DESTRUCTION_TIME, ISO8601Format.format(job.getDestructionTime())).toString(); }else return "{}"; } @@ -111,7 +112,7 @@ public class JSONSerializer extends UWSSerializer { @Override public String getStartTime(final UWSJob job, final boolean root) throws JSONException{ if (job.getDestructionTime() != null) - return Json4Uws.getJson(UWSJob.PARAM_START_TIME, UWSJob.dateFormat.format(job.getDestructionTime())).toString(); + return Json4Uws.getJson(UWSJob.PARAM_START_TIME, ISO8601Format.format(job.getDestructionTime())).toString(); else return "{}"; } @@ -119,7 +120,7 @@ public class JSONSerializer extends UWSSerializer { @Override public String getEndTime(final UWSJob job, final boolean root) throws JSONException{ if (job.getDestructionTime() != null) - return Json4Uws.getJson(UWSJob.PARAM_END_TIME, UWSJob.dateFormat.format(job.getDestructionTime())).toString(); + return Json4Uws.getJson(UWSJob.PARAM_END_TIME, ISO8601Format.format(job.getDestructionTime())).toString(); else return "{}"; } diff --git a/src/uws/job/serializer/UWSSerializer.java b/src/uws/job/serializer/UWSSerializer.java index 3641a69..663d086 100644 --- a/src/uws/job/serializer/UWSSerializer.java +++ b/src/uws/job/serializer/UWSSerializer.java @@ -22,6 +22,7 @@ package uws.job.serializer; import java.io.Serializable; +import uws.ISO8601Format; import uws.UWSException; import uws.job.ErrorSummary; import uws.job.JobList; @@ -41,7 +42,7 @@ import uws.service.UWSUrl; * </ul> * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (08/2014) + * @version 4.1 (09/2014) * * @see XMLSerializer * @see JSONSerializer @@ -50,7 +51,7 @@ public abstract class UWSSerializer implements Serializable { private static final long serialVersionUID = 1L; /** MIME type for XML: application/xml */ - public static final String MIME_TYPE_XML = "application/xml"; + public static final String MIME_TYPE_XML = "text/xml"; /** MIME type for JSON: application/json */ public static final String MIME_TYPE_JSON = "application/json"; /** MIME type for TEXT: text/plain */ @@ -95,16 +96,16 @@ public abstract class UWSSerializer implements Serializable { return job.getQuote() + ""; // START TIME: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_START_TIME)) - return (job.getStartTime() == null) ? "" : UWSJob.dateFormat.format(job.getStartTime()); + return (job.getStartTime() == null) ? "" : ISO8601Format.format(job.getStartTime()); // END TIME: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_END_TIME)) - return (job.getEndTime() == null) ? "" : UWSJob.dateFormat.format(job.getEndTime()); + return (job.getEndTime() == null) ? "" : ISO8601Format.format(job.getEndTime()); // EXECUTION DURATION: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_EXECUTION_DURATION)) return job.getExecutionDuration() + ""; // DESTRUCTION TIME: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_DESTRUCTION_TIME)) - return (job.getDestructionTime() == null) ? "" : UWSJob.dateFormat.format(job.getDestructionTime()); + return (job.getDestructionTime() == null) ? "" : ISO8601Format.format(job.getDestructionTime()); // PARAMETERS LIST: else if (firstAttribute.equalsIgnoreCase(UWSJob.PARAM_PARAMETERS)){ if (attributes.length <= 1) diff --git a/src/uws/job/serializer/XMLSerializer.java b/src/uws/job/serializer/XMLSerializer.java index c5322a4..fc33a64 100644 --- a/src/uws/job/serializer/XMLSerializer.java +++ b/src/uws/job/serializer/XMLSerializer.java @@ -16,13 +16,15 @@ package uws.job.serializer; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Iterator; +import uws.ISO8601Format; import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; @@ -34,8 +36,8 @@ import uws.service.UWSUrl; /** * Lets serializing any UWS resource in XML. * - * @author Grégory Mantelet (CDS) - * @version 05/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 4.1 (09/2014) */ public class XMLSerializer extends UWSSerializer { private static final long serialVersionUID = 1L; @@ -282,7 +284,7 @@ public class XMLSerializer extends UWSSerializer { if (job.getStartTime() == null) xml.append(" xsi:nil=\"true\" />"); else - xml.append(">").append(UWSJob.dateFormat.format(job.getStartTime())).append("</uws:startTime>"); + xml.append(">").append(ISO8601Format.format(job.getStartTime())).append("</uws:startTime>"); return xml.toString(); } @@ -293,7 +295,7 @@ public class XMLSerializer extends UWSSerializer { if (job.getEndTime() == null) xml.append(" xsi:nil=\"true\" />"); else - xml.append(">").append(UWSJob.dateFormat.format(job.getEndTime())).append("</uws:endTime>"); + xml.append(">").append(ISO8601Format.format(job.getEndTime())).append("</uws:endTime>"); return xml.toString(); } @@ -304,7 +306,7 @@ public class XMLSerializer extends UWSSerializer { if (job.getDestructionTime() == null) xml.append(" xsi:nil=\"true\" />"); else - xml.append(">").append(UWSJob.dateFormat.format(job.getDestructionTime())).append("</uws:destruction>"); + xml.append(">").append(ISO8601Format.format(job.getDestructionTime())).append("</uws:destruction>"); return xml.toString(); } diff --git a/src/uws/service/backup/DefaultUWSBackupManager.java b/src/uws/service/backup/DefaultUWSBackupManager.java index be9a0cf..f63c1f0 100644 --- a/src/uws/service/backup/DefaultUWSBackupManager.java +++ b/src/uws/service/backup/DefaultUWSBackupManager.java @@ -41,6 +41,7 @@ import org.json.JSONTokener; import org.json.JSONWriter; import org.json.Json4Uws; +import uws.ISO8601Format; import uws.UWSException; import uws.UWSToolBox; import uws.job.ErrorSummary; @@ -434,7 +435,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { out.object(); // Write the backup date: - out.key("date").value(UWSJob.dateFormat.format(new Date())); + out.key("date").value(ISO8601Format.format(new Date())); // Write the description of the user: out.key("user").value(getJSONUser(user)); @@ -805,7 +806,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { else if (key.equalsIgnoreCase(UWSJob.PARAM_DESTRUCTION_TIME)){ try{ tmp = json.getString(key); - inputParams.put(UWSJob.PARAM_DESTRUCTION_TIME, UWSJob.dateFormat.parse(tmp)); + inputParams.put(UWSJob.PARAM_DESTRUCTION_TIME, ISO8601Format.parseToDate(tmp)); }catch(ParseException pe){ getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Incorrect date format for the '" + key + "' parameter!", pe); } @@ -814,7 +815,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { else if (key.equalsIgnoreCase(UWSJob.PARAM_START_TIME)){ tmp = json.getString(key); try{ - Date d = UWSJob.dateFormat.parse(tmp); + Date d = ISO8601Format.parseToDate(tmp); startTime = d.getTime(); }catch(ParseException pe){ getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Incorrect date format for the '" + key + "' parameter!", pe); @@ -824,7 +825,7 @@ public class DefaultUWSBackupManager implements UWSBackupManager { else if (key.equalsIgnoreCase(UWSJob.PARAM_END_TIME)){ tmp = json.getString(key); try{ - Date d = UWSJob.dateFormat.parse(tmp); + Date d = ISO8601Format.parseToDate(tmp); endTime = d.getTime(); }catch(ParseException pe){ getLogger().logUWS(LogLevel.ERROR, json, "RESTORATION", "Incorrect date format for the '" + key + "' parameter!", pe); diff --git a/test/uws/TestISO8601Format.java b/test/uws/TestISO8601Format.java new file mode 100644 index 0000000..b397c85 --- /dev/null +++ b/test/uws/TestISO8601Format.java @@ -0,0 +1,163 @@ +package uws; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.text.ParseException; +import java.util.TimeZone; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestISO8601Format { + + private final long date = 1411737870325L; // Fri Sep 26 15:24:30 CEST 2014 = 2014-09-26T15:24:30.325+02:00 = 1411737870325 ms + private final long dateAlone = 1411682400000L; + + private final long oldDate = -3506029200000L; // Thu Nov 25 00:00:00 CET 1858 = 1858-11-25T00:00:00+01:00 = -3506029200000 ms + + private static boolean displayMS; + private static boolean displayTZ; + private static String targetTZ = null; + + @BeforeClass + public static void setUpBeforeClass() throws Exception{ + displayMS = ISO8601Format.displayMilliseconds; + displayTZ = ISO8601Format.displayTimeZone; + targetTZ = ISO8601Format.targetTimeZone; + } + + @Before + public void setUp() throws Exception{ + ISO8601Format.displayMilliseconds = false; + ISO8601Format.displayTimeZone = true; + ISO8601Format.targetTimeZone = "Europe/Berlin"; + } + + @AfterClass + public static void tearDownAfterClass() throws Exception{ + ISO8601Format.displayMilliseconds = displayMS; + ISO8601Format.displayTimeZone = displayTZ; + ISO8601Format.targetTimeZone = targetTZ; + } + + @Test + public void testFormatDate(){ + // Special case: reference for the millisecond representation of dates (1st January 1970): + assertEquals("1970-01-01T01:00:00+01:00", ISO8601Format.format(0)); + assertEquals("1970-01-01T00:00:00Z", ISO8601Format.formatInUTC(0)); + + // Special case: old date (25th November 1858): + assertEquals("1858-11-25T00:00:00+01:00", ISO8601Format.format(oldDate)); + assertEquals("1858-11-24T23:00:00Z", ISO8601Format.formatInUTC(oldDate)); + + // Tests of: FORMAT(Date) && FORMAT(Date, boolean withTimestamp): + assertEquals("2014-09-26T15:24:30+02:00", ISO8601Format.format(date)); + assertEquals(ISO8601Format.format(date), ISO8601Format.format(date, true)); + assertEquals("2014-09-26T15:24:30", ISO8601Format.format(date, false)); + + // Tests of: FORMAT_IN_UTC(Date) && FORMAT_IN_UTC(Date, boolean withTimestamp): + assertEquals("2014-09-26T13:24:30Z", ISO8601Format.formatInUTC(date)); + assertEquals(ISO8601Format.formatInUTC(date), ISO8601Format.formatInUTC(date, true)); + assertEquals("2014-09-26T13:24:30", ISO8601Format.formatInUTC(date, false)); + + // Test with a different time zone: + assertEquals("2014-09-26T17:24:30+04:00", ISO8601Format.format(date, "Indian/Reunion", true, false)); + + // Test with no specified different time zone (the chosen time zone should be the local one): + assertEquals(ISO8601Format.format(date, TimeZone.getDefault().getID(), true, false), ISO8601Format.format(date, null, true, false)); + + // Test with display of milliseconds: + assertEquals("2014-09-26T15:24:30.325+02:00", ISO8601Format.format(date, null, true, true)); + assertEquals("2014-09-26T15:24:30.325", ISO8601Format.format(date, null, false, true)); + + // Same tests but in the UTC time zone: + assertEquals("2014-09-26T13:24:30.325Z", ISO8601Format.format(date, "UTC", true, true)); + assertEquals("2014-09-26T13:24:30.325", ISO8601Format.format(date, "UTC", false, true)); + } + + @Test + public void testParse(){ + // Special case: NULL + try{ + ISO8601Format.parse(null); + fail("Parse can not theoretically work without a string"); + }catch(Throwable t){ + assertEquals(NullPointerException.class, t.getClass()); + } + + // Special case: "" + try{ + ISO8601Format.parse(""); + fail("Parse can not theoretically work without a non-empty string"); + }catch(Throwable t){ + assertEquals(ParseException.class, t.getClass()); + assertEquals("Invalid date format: \"\"! An ISO8601 date was expected.", t.getMessage()); + } + + // Special case: anything stupid rather than a valid date + try{ + ISO8601Format.parse("stupid thing"); + fail("Parse can not theoretically work without a valid string date"); + }catch(Throwable t){ + assertEquals(ParseException.class, t.getClass()); + assertEquals("Invalid date format: \"stupid thing\"! An ISO8601 date was expected.", t.getMessage()); + } + + try{ + // Special case: reference for the millisecond representation of dates (1st January 1970): + assertEquals(0, ISO8601Format.parse("1970-01-01T01:00:00+01:00")); + assertEquals(0, ISO8601Format.parse("1970-01-01T00:00:00Z")); + + // Special case: old date (25th November 1858): + assertEquals(oldDate, ISO8601Format.parse("1858-11-25T00:00:00+01:00")); + assertEquals(oldDate, ISO8601Format.parse("1858-11-24T23:00:00Z")); + + // Test with a perfectly valid date in ISO8601: + assertEquals(dateAlone, ISO8601Format.parse("2014-09-26")); + assertEquals(date, ISO8601Format.parse("2014-09-26T15:24:30.325+02:00")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26T15:24:30+02:00")); + + // Test with Z as time zone (UTC): + assertEquals(date, ISO8601Format.parse("2014-09-26T13:24:30.325Z")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26T13:24:30Z")); + + // If no time zone is specified, the local one should be used: + assertEquals(date, ISO8601Format.parse("2014-09-26T15:24:30.325")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26T15:24:30")); + + // All the previous tests without the _ between days, month, and years: + assertEquals(0, ISO8601Format.parse("19700101T01:00:00+01:00")); + assertEquals(0, ISO8601Format.parse("19700101T00:00:00Z")); + assertEquals(oldDate, ISO8601Format.parse("18581125T00:00:00+01:00")); + assertEquals(oldDate, ISO8601Format.parse("18581124T23:00:00Z")); + assertEquals(dateAlone, ISO8601Format.parse("20140926")); + assertEquals(date, ISO8601Format.parse("20140926T15:24:30.325+02:00")); + assertEquals(date - 325, ISO8601Format.parse("20140926T15:24:30+02:00")); + assertEquals(date, ISO8601Format.parse("20140926T13:24:30.325Z")); + assertEquals(date - 325, ISO8601Format.parse("20140926T13:24:30Z")); + assertEquals(date, ISO8601Format.parse("20140926T15:24:30.325")); + assertEquals(date - 325, ISO8601Format.parse("20140926T15:24:30")); + + // All the previous tests without the : between hours, minutes and seconds: + assertEquals(0, ISO8601Format.parse("1970-01-01T010000+0100")); + assertEquals(oldDate, ISO8601Format.parse("1858-11-25T000000+0100")); + assertEquals(date, ISO8601Format.parse("2014-09-26T152430.325+0200")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26T152430+0200")); + + // All the previous tests by replacing the T between date and time by a space: + assertEquals(0, ISO8601Format.parse("1970-01-01 00:00:00Z")); + assertEquals(oldDate, ISO8601Format.parse("1858-11-24 23:00:00Z")); + assertEquals(date, ISO8601Format.parse("2014-09-26 13:24:30.325Z")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26 13:24:30Z")); + assertEquals(date, ISO8601Format.parse("2014-09-26 15:24:30.325")); + assertEquals(date - 325, ISO8601Format.parse("2014-09-26 15:24:30")); + + }catch(ParseException ex){ + ex.printStackTrace(System.err); + } + } + +} -- GitLab