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&eacute;gory Mantelet (CDS)
- * @version 05/2012
+ * @author Gr&eacute;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&eacute;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&eacute;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&eacute;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&eacute;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&eacute;gory Mantelet (CDS)
- * @version 05/2012
+ * @author Gr&eacute;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