From 2a1e57863b2e71b8dd60ca4bb339ed9502f8ac46 Mon Sep 17 00:00:00 2001
From: gmantele <gmantele@ari.uni-heidelberg.de>
Date: Thu, 13 Nov 2014 18:15:51 +0100
Subject: [PATCH] [UWS,TAP] Review the standard parameters checking.
 Particularly, because before, the default value was never used while no value
 was specified by the user.

---
 src/tap/AbstractTAPFactory.java               |   6 +-
 src/tap/TAPFactory.java                       |   6 +-
 src/tap/parameters/FormatController.java      |   2 +-
 src/tap/parameters/MaxRecController.java      |  34 ++--
 .../TAPDestructionTimeController.java         |  47 +++--
 .../TAPExecutionDurationController.java       |  44 +++--
 src/tap/parameters/TAPParameters.java         |  83 +++-----
 src/uws/job/JobThread.java                    |   2 +-
 .../parameters/DestructionTimeController.java |  54 +++--
 .../ExecutionDurationController.java          |  87 +++++---
 .../job/parameters/InputParamController.java  |   1 +
 src/uws/job/parameters/UWSParameters.java     |   1 -
 src/uws/service/UWS.java                      |   6 +-
 .../parameters/ServiceConnectionOfTest.java   | 161 +++++++++++++++
 test/tap/parameters/TestFormatController.java |  89 +++++++++
 test/tap/parameters/TestMaxRecController.java | 142 +++++++++++++
 .../TestTAPDestructionTimeController.java     | 187 ++++++++++++++++++
 .../TestTAPExecutionDurationController.java   | 136 +++++++++++++
 .../TestDestructionTimeController.java        | 186 +++++++++++++++++
 .../TestExecutionDurationController.java      | 134 +++++++++++++
 20 files changed, 1253 insertions(+), 155 deletions(-)
 create mode 100644 test/tap/parameters/ServiceConnectionOfTest.java
 create mode 100644 test/tap/parameters/TestFormatController.java
 create mode 100644 test/tap/parameters/TestMaxRecController.java
 create mode 100644 test/tap/parameters/TestTAPDestructionTimeController.java
 create mode 100644 test/tap/parameters/TestTAPExecutionDurationController.java
 create mode 100644 test/uws/job/parameters/TestDestructionTimeController.java
 create mode 100644 test/uws/job/parameters/TestExecutionDurationController.java

diff --git a/src/tap/AbstractTAPFactory.java b/src/tap/AbstractTAPFactory.java
index 36984fa..203a202 100644
--- a/src/tap/AbstractTAPFactory.java
+++ b/src/tap/AbstractTAPFactory.java
@@ -53,7 +53,7 @@ import adql.query.ADQLQuery;
  * Only the functions related with the database connection stay abstract.
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (10/2014)
+ * @version 2.0 (11/2014)
  */
 public abstract class AbstractTAPFactory extends TAPFactory {
 
@@ -277,7 +277,7 @@ public abstract class AbstractTAPFactory extends TAPFactory {
 	@Override
 	public TAPParameters createTAPParameters(final HttpServletRequest request) throws TAPException{
 		try{
-			return new TAPParameters(request, service, getExpectedAdditionalParameters(), getInputParamControllers());
+			return new TAPParameters(request, service);
 		}catch(UWSException ue){
 			throw new TAPException(ue);
 		}
@@ -296,7 +296,7 @@ public abstract class AbstractTAPFactory extends TAPFactory {
 	@Override
 	public TAPParameters createTAPParameters(final Map<String,Object> params) throws TAPException{
 		try{
-			return new TAPParameters(service, params, getExpectedAdditionalParameters(), getInputParamControllers());
+			return new TAPParameters(service, params);
 		}catch(UWSException ue){
 			throw new TAPException(ue);
 		}
diff --git a/src/tap/TAPFactory.java b/src/tap/TAPFactory.java
index de00df9..f0ba06f 100644
--- a/src/tap/TAPFactory.java
+++ b/src/tap/TAPFactory.java
@@ -37,7 +37,7 @@ import uws.job.Result;
 import uws.job.UWSJob;
 import uws.job.parameters.UWSParameters;
 import uws.job.user.JobOwner;
-import uws.service.AbstractUWSFactory;
+import uws.service.UWSFactory;
 import uws.service.UWSService;
 import uws.service.backup.UWSBackupManager;
 import uws.service.error.ServiceErrorWriter;
@@ -59,9 +59,9 @@ import adql.query.ADQLQuery;
  * </ul>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (09/2014)
+ * @version 2.0 (11/2014)
  */
-public abstract class TAPFactory extends AbstractUWSFactory {
+public abstract class TAPFactory implements UWSFactory {
 
 	/** Connection to the TAP service ; it provides all important service configuration information. */
 	protected final ServiceConnection service;
diff --git a/src/tap/parameters/FormatController.java b/src/tap/parameters/FormatController.java
index 93e654b..a48339a 100644
--- a/src/tap/parameters/FormatController.java
+++ b/src/tap/parameters/FormatController.java
@@ -78,7 +78,7 @@ public class FormatController implements InputParamController {
 	@Override
 	public Object check(Object format) throws UWSException{
 		if (format == null)
-			return null;
+			return getDefault();
 
 		if (format instanceof String){
 			String strFormat = ((String)format).trim();
diff --git a/src/tap/parameters/MaxRecController.java b/src/tap/parameters/MaxRecController.java
index 6aa4694..da3b94b 100644
--- a/src/tap/parameters/MaxRecController.java
+++ b/src/tap/parameters/MaxRecController.java
@@ -32,17 +32,24 @@ import uws.job.parameters.InputParamController;
  * 
  * <p><i>Note:
  * 	By default, this parameter can be modified by anyone without any limitation.
+ * 	The default and maximum value is set by default to {@link TAPJob#UNLIMITED_MAX_REC}.
+ * </i></p>
+ * 
+ * <p><i>Note:
+ * 	The special value 0 means that just the metadata of the result must be returned.
+ * 	Considering the meaning of this value, it will not be considered as an {@link TAPJob#UNLIMITED_MAX_REC},
+ * 	but like a valid value. The maximum value can then be also 0.
  * </i></p>
  * 
  * <p>The logic of the output limit is set in this class. Here it is:</p>
  * <ul>
- * 	<li>If no value is specified by the TAP client, none is returned.</li>
- *  <li>If no default value is provided, no default limitation is set (={@link TAPJob#UNLIMITED_MAX_REC}).</li>
- *  <li>If no maximum value is provided, there is no output limit (={@link TAPJob#UNLIMITED_MAX_REC}).</li>
+ * 	<li>If no value is specified by the TAP client, the default value is returned.</li>
+ *  <li>If no default value is provided, the maximum output limit is returned.</li>
+ *  <li>If no maximum value is provided, there is no limit (={@link TAPJob#UNLIMITED_MAX_REC}).</li>
  * </ul>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (09/2014)
+ * @version 2.0 (11/2014)
  */
 public class MaxRecController implements InputParamController {
 
@@ -63,13 +70,18 @@ public class MaxRecController implements InputParamController {
 
 	@Override
 	public final Object getDefault(){
-		// If a default output limit is set by the TAP service connection, return it:
+		// Get the default output limit:
+		int defaultLimit = TAPJob.UNLIMITED_MAX_REC;
 		if (service.getOutputLimit() != null && service.getOutputLimit().length >= 2 && service.getOutputLimitType() != null && service.getOutputLimitType().length == service.getOutputLimit().length){
 			if (service.getOutputLimit()[0] > 0 && service.getOutputLimitType()[0] == LimitUnit.rows)
-				return service.getOutputLimit()[0];
+				defaultLimit = service.getOutputLimit()[0];
 		}
-		// Otherwise, return no limitation:
-		return TAPJob.UNLIMITED_MAX_REC;
+
+		// Get the maximum output limit, for comparison:
+		int maxLimit = getMaxOutputLimit();
+
+		// Ensure the default limit is less or equal the maximum limit:
+		return (defaultLimit < 0 || (maxLimit >= 0 && defaultLimit > maxLimit)) ? maxLimit : defaultLimit;
 	}
 
 	/**
@@ -91,7 +103,7 @@ public class MaxRecController implements InputParamController {
 	public Object check(Object value) throws UWSException{
 		// If no limit is provided by the TAP client, none is returned:
 		if (value == null)
-			return null;
+			return getDefault();
 
 		// Parse the provided limit:
 		int maxOutputLimit = getMaxOutputLimit();
@@ -109,11 +121,11 @@ public class MaxRecController implements InputParamController {
 			throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"maxrec\": class \"" + value.getClass().getName() + "\"! It should be an integer or a string containing only an integer value.");
 
 		// A negative output limit is considered as an unlimited output limit:
-		if (maxRec < TAPJob.UNLIMITED_MAX_REC)
+		if (maxRec < 0)
 			maxRec = TAPJob.UNLIMITED_MAX_REC;
 
 		// If the limit is greater than the maximum one, an exception is thrown:
-		if (maxRec == TAPJob.UNLIMITED_MAX_REC || maxRec > maxOutputLimit)
+		if (maxRec < 0 || (maxOutputLimit >= 0 && maxRec > maxOutputLimit))
 			maxRec = maxOutputLimit;
 
 		return maxRec;
diff --git a/src/tap/parameters/TAPDestructionTimeController.java b/src/tap/parameters/TAPDestructionTimeController.java
index 0c3a403..e5742ed 100644
--- a/src/tap/parameters/TAPDestructionTimeController.java
+++ b/src/tap/parameters/TAPDestructionTimeController.java
@@ -27,7 +27,6 @@ import java.util.Date;
 import tap.ServiceConnection;
 import uws.ISO8601Format;
 import uws.UWSException;
-import uws.job.UWSJob;
 import uws.job.parameters.DestructionTimeController.DateField;
 import uws.job.parameters.InputParamController;
 
@@ -40,8 +39,15 @@ import uws.job.parameters.InputParamController;
  * 	There is no default value (that means jobs may stay forever).
  * </i></p>
  * 
+ * <p>The logic of the destruction time is set in this class. Here it is:</p>
+ * <ul>
+ * 	<li>If no value is specified by the UWS client, the default value is returned.</li>
+ *  <li>If no default value is provided, the maximum destruction date is returned.</li>
+ *  <li>If no maximum value is provided, there is no destruction.</li>
+ * </ul>
+ * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (09/2014)
+ * @version 2.0 (11/2014)
  */
 public class TAPDestructionTimeController implements InputParamController {
 
@@ -90,17 +96,23 @@ public class TAPDestructionTimeController implements InputParamController {
 
 	@Override
 	public final Object getDefault(){
+		// Get the default period and ensure it is always less or equal the maximum period, if any:
 		int defaultPeriod = getDefaultRetentionPeriod();
+		int maxPeriod = getMaxRetentionPeriod();
+		if (defaultPeriod <= 0 || (maxPeriod > 0 && defaultPeriod > maxPeriod))
+			defaultPeriod = maxPeriod;
+
+		// Build and return the date:
 		if (defaultPeriod > 0){
 			Calendar date = Calendar.getInstance();
 			try{
 				date.add(DateField.SECOND.getFieldIndex(), defaultPeriod);
 				return date.getTime();
-			}catch(ArrayIndexOutOfBoundsException ex){
-				return null;
-			}
-		}else
-			return null;
+			}catch(ArrayIndexOutOfBoundsException ex){}
+		}
+
+		// If no default period is specified or if an exception occurs, the maximum destruction time must be returned:
+		return getMaxDestructionTime();
 	}
 
 	/**
@@ -123,24 +135,29 @@ public class TAPDestructionTimeController implements InputParamController {
 	 * @return The maximum destruction time (<i>null</i> means that jobs may stay forever).
 	 */
 	public final Date getMaxDestructionTime(){
+		// Get the maximum period:
 		int maxPeriod = getMaxRetentionPeriod();
+
+		// Build and return the maximum destruction date:
 		if (maxPeriod > 0){
 			Calendar date = Calendar.getInstance();
 			try{
 				date.add(DateField.SECOND.getFieldIndex(), maxPeriod);
 				return date.getTime();
-			}catch(ArrayIndexOutOfBoundsException ex){
-				return null;
-			}
-		}else
-			return null;
+			}catch(ArrayIndexOutOfBoundsException ex){}
+		}
+
+		// If no maximum period is specified or if an exception occurs, NULL must be returned:
+		return null;
 	}
 
 	@Override
 	public Object check(Object value) throws UWSException{
+		// If NULL value, return the default value:
 		if (value == null)
-			return null;
+			return getDefault();
 
+		// Parse the given date:
 		Date date = null;
 		if (value instanceof Date)
 			date = (Date)value;
@@ -154,10 +171,12 @@ public class TAPDestructionTimeController implements InputParamController {
 		}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 formatted in ISO8601 (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional).");
 
+		// Ensure the date is before the maximum destruction time (from now):
 		Date maxDate = getMaxDestructionTime();
 		if (maxDate != null && date.after(maxDate))
-			throw new UWSException(UWSException.BAD_REQUEST, "The TAP service limits the destruction interval (since now) to " + getMaxRetentionPeriod() + " s !");
+			date = maxDate;
 
+		// Return the parsed date
 		return date;
 	}
 
diff --git a/src/tap/parameters/TAPExecutionDurationController.java b/src/tap/parameters/TAPExecutionDurationController.java
index 63d15ba..6c158d0 100644
--- a/src/tap/parameters/TAPExecutionDurationController.java
+++ b/src/tap/parameters/TAPExecutionDurationController.java
@@ -34,8 +34,15 @@ import uws.job.parameters.InputParamController;
  * 	The default value is {@link TAPJob#UNLIMITED_DURATION}.
  * </i></p>
  * 
+ * <p>The logic of the execution duration is set in this class. Here it is:</p>
+ * <ul>
+ * 	<li>If no value is specified by the TAP client, the default value is returned.</li>
+ *  <li>If no default value is provided, the maximum duration is returned.</li>
+ *  <li>If no maximum value is provided, there is no limit (={@link TAPJob#UNLIMITED_DURATION}).</li>
+ * </ul>
+ * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (09/2014)
+ * @version 2.0 (11/2014)
  */
 public class TAPExecutionDurationController implements InputParamController {
 
@@ -70,11 +77,14 @@ public class TAPExecutionDurationController implements InputParamController {
 
 	@Override
 	public final Object getDefault(){
-		if (service.getExecutionDuration() != null && service.getExecutionDuration().length >= 2){
-			if (service.getExecutionDuration()[0] > 0)
-				return service.getExecutionDuration()[0];
-		}
-		return TAPJob.UNLIMITED_DURATION;
+		// Get the default value from the service connection:
+		long defaultVal = TAPJob.UNLIMITED_DURATION;
+		if (service.getExecutionDuration() != null && service.getExecutionDuration().length >= 2)
+			defaultVal = service.getExecutionDuration()[0];
+
+		// The default value is also limited by the maximum value if any:
+		long maxVal = getMaxDuration();
+		return (defaultVal <= 0 || (maxVal > 0 && defaultVal > maxVal)) ? maxVal : defaultVal;
 	}
 
 	/**
@@ -91,15 +101,20 @@ public class TAPExecutionDurationController implements InputParamController {
 	}
 
 	@Override
-	public Object check(Object value) throws UWSException{
+	public Object check(final Object value) throws UWSException{
+		// If no value, return the default one:
 		if (value == null)
-			return null;
+			return getDefault();
 
-		long defaultDuration = ((Long)getDefault()).longValue(), maxDuration = getMaxDuration();
-		Long duration;
+		// Get the default and maximum durations for comparison:
+		long defaultDuration = (Long)getDefault(), maxDuration = getMaxDuration();
 
+		// Parse the given duration:		
+		Long duration;
 		if (value instanceof Long)
 			duration = (Long)value;
+		else if (value instanceof Integer)
+			duration = (long)(Integer)value;
 		else if (value instanceof String){
 			try{
 				duration = Long.parseLong((String)value);
@@ -109,10 +124,13 @@ public class TAPExecutionDurationController implements InputParamController {
 		}else
 			throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the parameter \"executionduration\": class \"" + value.getClass().getName() + "\"! It should be long or a string containing only a long value.");
 
-		if (duration < TAPJob.UNLIMITED_DURATION)
+		// A negative value must be considered as an unlimited duration:
+		if (duration <= 0)
 			duration = TAPJob.UNLIMITED_DURATION;
-		else if (maxDuration > TAPJob.UNLIMITED_DURATION && duration > maxDuration)
-			throw new UWSException(UWSException.BAD_REQUEST, "The TAP service limits the execution duration to maximum " + maxDuration + " seconds !");
+
+		// Ensure the given value is less than the maximum duration:
+		if (maxDuration > 0 && (duration > maxDuration || duration <= 0))
+			duration = maxDuration;
 
 		return duration;
 	}
diff --git a/src/tap/parameters/TAPParameters.java b/src/tap/parameters/TAPParameters.java
index 6eb4f09..21a2e69 100644
--- a/src/tap/parameters/TAPParameters.java
+++ b/src/tap/parameters/TAPParameters.java
@@ -16,15 +16,17 @@ package tap.parameters;
  * You should have received a copy of the GNU Lesser General Public License
  * along with TAPLibrary.  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.File;
 import java.io.IOException;
-import java.util.Collection;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
@@ -44,15 +46,13 @@ import com.oreilly.servlet.multipart.FileRenamePolicy;
 /**
  * This class describes all defined parameters of a TAP request.
  * 
- * @author Gr&eacute;gory Mantelet (CDS)
- * @version 06/2012
+ * @author Gr&eacute;gory Mantelet (CDS;ARI)
+ * @version 2.0 (11/2014)
  */
 public class TAPParameters extends UWSParameters {
 
-	/**
-	 * All the TAP parameters.
-	 */
-	protected static final String[] TAP_PARAMETERS = new String[]{TAPJob.PARAM_REQUEST,TAPJob.PARAM_LANGUAGE,TAPJob.PARAM_VERSION,TAPJob.PARAM_FORMAT,TAPJob.PARAM_QUERY,TAPJob.PARAM_MAX_REC,TAPJob.PARAM_UPLOAD};
+	/** All the TAP parameters. */
+	protected static final List<String> TAP_PARAMETERS = Arrays.asList(new String[]{TAPJob.PARAM_REQUEST,TAPJob.PARAM_LANGUAGE,TAPJob.PARAM_VERSION,TAPJob.PARAM_FORMAT,TAPJob.PARAM_QUERY,TAPJob.PARAM_MAX_REC,TAPJob.PARAM_UPLOAD});
 
 	/** Part of HTTP content type header. */
 	public static final String MULTIPART = "multipart/";
@@ -61,21 +61,13 @@ public class TAPParameters extends UWSParameters {
 	protected TableLoader[] tablesToUpload = null;
 
 	public TAPParameters(final ServiceConnection service){
-		this(service, null, null);
-	}
-
-	public TAPParameters(final ServiceConnection service, final Collection<String> expectedAdditionalParams, final Map<String,InputParamController> inputParamControllers){
-		super(expectedAdditionalParams, inputParamControllers);
+		super(TAP_PARAMETERS, null);
 		initDefaultTAPControllers(service);
 	}
 
 	public TAPParameters(final HttpServletRequest request, final ServiceConnection service) throws UWSException, TAPException{
-		this(request, service, null, null);
-	}
+		this(service);
 
-	@SuppressWarnings("unchecked")
-	public TAPParameters(final HttpServletRequest request, final ServiceConnection service, final Collection<String> expectedAdditionalParams, final Map<String,InputParamController> inputParamControllers) throws UWSException, TAPException{
-		this(service, expectedAdditionalParams, inputParamControllers);
 		MultipartRequest multipart = null;
 
 		// Multipart HTTP parameters:
@@ -91,6 +83,7 @@ public class TAPParameters extends UWSParameters {
 						return new File(file.getParentFile(), (new Date()).toString() + "_" + file.getName());
 					}
 				});
+				@SuppressWarnings("unchecked")
 				Enumeration<String> e = multipart.getParameterNames();
 				while(e.hasMoreElements()){
 					String param = e.nextElement();
@@ -124,11 +117,7 @@ public class TAPParameters extends UWSParameters {
 	}
 
 	public TAPParameters(final ServiceConnection service, final Map<String,Object> params) throws UWSException, TAPException{
-		this(service, params, null, null);
-	}
-
-	public TAPParameters(final ServiceConnection service, final Map<String,Object> params, final Collection<String> expectedAdditionalParams, final Map<String,InputParamController> inputParamControllers) throws UWSException, TAPException{
-		super(params, expectedAdditionalParams, inputParamControllers);
+		super(params, TAP_PARAMETERS, null);
 		initDefaultTAPControllers(service);
 	}
 
@@ -137,44 +126,16 @@ public class TAPParameters extends UWSParameters {
 		return new HashMap<String,InputParamController>(10);
 	}
 
-	protected < R > void initDefaultTAPControllers(final ServiceConnection service){
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_EXECUTION_DURATION))
-			mapParamControllers.put(TAPJob.PARAM_EXECUTION_DURATION, new TAPExecutionDurationController(service));
-
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_DESTRUCTION_TIME))
-			mapParamControllers.put(TAPJob.PARAM_DESTRUCTION_TIME, new TAPDestructionTimeController(service));
-
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_REQUEST))
-			mapParamControllers.put(TAPJob.PARAM_REQUEST, new StringParamController(TAPJob.PARAM_REQUEST, null, new String[]{TAPJob.REQUEST_DO_QUERY,TAPJob.REQUEST_GET_CAPABILITIES}, true));
-
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_LANGUAGE))
-			mapParamControllers.put(TAPJob.PARAM_LANGUAGE, new StringParamController(TAPJob.PARAM_LANGUAGE, TAPJob.LANG_ADQL, null, true));
-
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_VERSION))
-			mapParamControllers.put(TAPJob.PARAM_VERSION, new StringParamController(TAPJob.PARAM_VERSION, TAPJob.VERSION_1_0, new String[]{TAPJob.VERSION_1_0}, true));
-
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_QUERY))
-			mapParamControllers.put(TAPJob.PARAM_QUERY, new StringParamController(TAPJob.PARAM_QUERY));
-
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_UPLOAD))
-			mapParamControllers.put(TAPJob.PARAM_UPLOAD, new StringParamController(TAPJob.PARAM_UPLOAD));
-
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_FORMAT))
-			mapParamControllers.put(TAPJob.PARAM_FORMAT, new FormatController(service));
-
-		if (!mapParamControllers.containsKey(TAPJob.PARAM_MAX_REC))
-			mapParamControllers.put(TAPJob.PARAM_MAX_REC, new MaxRecController(service));
-	}
-
-	@Override
-	protected String normalizeParamName(String name){
-		if (name != null && !name.trim().isEmpty()){
-			for(String tapParam : TAP_PARAMETERS){
-				if (name.equalsIgnoreCase(tapParam))
-					return tapParam;
-			}
-		}
-		return super.normalizeParamName(name);
+	protected void initDefaultTAPControllers(final ServiceConnection service){
+		mapParamControllers.put(TAPJob.PARAM_EXECUTION_DURATION, new TAPExecutionDurationController(service));
+		mapParamControllers.put(TAPJob.PARAM_DESTRUCTION_TIME, new TAPDestructionTimeController(service));
+		mapParamControllers.put(TAPJob.PARAM_REQUEST, new StringParamController(TAPJob.PARAM_REQUEST, null, new String[]{TAPJob.REQUEST_DO_QUERY,TAPJob.REQUEST_GET_CAPABILITIES}, true));
+		mapParamControllers.put(TAPJob.PARAM_LANGUAGE, new StringParamController(TAPJob.PARAM_LANGUAGE, TAPJob.LANG_ADQL, null, true));
+		mapParamControllers.put(TAPJob.PARAM_VERSION, new StringParamController(TAPJob.PARAM_VERSION, TAPJob.VERSION_1_0, new String[]{TAPJob.VERSION_1_0}, true));
+		mapParamControllers.put(TAPJob.PARAM_QUERY, new StringParamController(TAPJob.PARAM_QUERY));
+		mapParamControllers.put(TAPJob.PARAM_UPLOAD, new StringParamController(TAPJob.PARAM_UPLOAD));
+		mapParamControllers.put(TAPJob.PARAM_FORMAT, new FormatController(service));
+		mapParamControllers.put(TAPJob.PARAM_MAX_REC, new MaxRecController(service));
 	}
 
 	@Override
diff --git a/src/uws/job/JobThread.java b/src/uws/job/JobThread.java
index 538e0ba..5f2bef3 100644
--- a/src/uws/job/JobThread.java
+++ b/src/uws/job/JobThread.java
@@ -282,7 +282,7 @@ public abstract class JobThread extends Thread {
 			setError(error);
 
 		}catch(IOException ioe){
-			job.getLogger().logThread(LogLevel.ERROR, this, "SET_ERROR", "The stack trace of a UWSException had not been written !", ioe);
+			job.getLogger().logThread(LogLevel.ERROR, this, "SET_ERROR", "The stack trace of a UWSException had not been written!", ioe);
 			setError(new ErrorSummary(ue.getMessage(), ue.getUWSErrorType()));
 		}
 	}
diff --git a/src/uws/job/parameters/DestructionTimeController.java b/src/uws/job/parameters/DestructionTimeController.java
index 35c9443..bf86999 100644
--- a/src/uws/job/parameters/DestructionTimeController.java
+++ b/src/uws/job/parameters/DestructionTimeController.java
@@ -34,19 +34,24 @@ import uws.UWSException;
  * 	Moreover you can indicate whether the destruction time of jobs can be modified by the user or not.
  * </p>
  * 
- * <p>
- * 	<i><u>Notes:</u>
- * 		<ul>
- * 			<li>By default, the destruction time can be modified by anyone without any limitation.
- * 				There is no default value (that means jobs may stay forever).</li>
- * 			<li>You can specify a destruction time (default or maximum value) in two ways:
- * 				by an exact date-time or by an interval of time from the initialization (expressed in the second, minutes, hours, days, months or years).</li>
- * 		</ul>
- * 	</i>
- * </p>
+ * <p><i><u>Notes:</u>
+ * 	<ul>
+ * 		<li>By default, the destruction time can be modified by anyone without any limitation.
+ * 			There is no default value (that means jobs may stay forever).</li>
+ * 		<li>You can specify a destruction time (default or maximum value) in two ways:
+ * 			by an exact date-time or by an interval of time from the initialization (expressed in the second, minutes, hours, days, months or years).</li>
+ * 	</ul>
+ * </i></p>
+ * 
+ * <p>The logic of the destruction time is set in this class. Here it is:</p>
+ * <ul>
+ * 	<li>If no value is specified by the UWS client, the default value is returned.</li>
+ *  <li>If no default value is provided, the maximum destruction date is returned.</li>
+ *  <li>If no maximum value is provided, there is no destruction.</li>
+ * </ul>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 4.1 (09/2014)
+ * @version 4.1 (11/2014)
  */
 public class DestructionTimeController implements InputParamController, Serializable {
 	private static final long serialVersionUID = 1L;
@@ -55,7 +60,7 @@ public class DestructionTimeController implements InputParamController, Serializ
 	 * Represents a date/time field.
 	 * 
 	 * @author Gr&eacute;gory Mantelet (CDS)
-	 * @version 02/2011
+	 * @version 4.0 (02/2011)
 	 * 
 	 * @see Calendar
 	 */
@@ -94,10 +99,12 @@ public class DestructionTimeController implements InputParamController, Serializ
 	protected boolean allowModification = true;
 
 	@Override
-	public Object check(Object value) throws UWSException{
+	public Object check(final Object value) throws UWSException{
+		// If no value, return the default one:
 		if (value == null)
-			return null;
+			return getDefault();
 
+		// Otherwise, parse the date:
 		Date date = null;
 		if (value instanceof Date)
 			date = (Date)value;
@@ -111,16 +118,19 @@ public class DestructionTimeController implements InputParamController, Serializ
 		}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 formatted in IS8601 (\"yyyy-MM-dd'T'hh:mm:ss[.sss]['Z'|[+|-]hh:mm]\", fields inside brackets are optional).");
 
+		// Compare it to the maximum destruction time: if after, set the date to the maximum allowed date:
 		Date maxDate = getMaxDestructionTime();
 		if (maxDate != null && date.after(maxDate))
-			throw new UWSException(UWSException.BAD_REQUEST, "The UWS limits " + ((defaultInterval > NO_INTERVAL) ? ("the DESTRUCTION INTERVAL (since now) to " + maxInterval + " " + maxIntervalField.name().toLowerCase() + "s") : ("the DESTRUCTION TIME to " + maxDate)) + " !");
+			date = maxDate;
 
+		// Return the parsed date:
 		return date;
 	}
 
 	@Override
 	public Object getDefault(){
-		return getDefaultDestructionTime();
+		Date defaultDate = getDefaultDestructionTime();
+		return (defaultDate == null) ? getMaxDestructionTime() : defaultDate;
 	}
 
 	/* ***************** */
@@ -306,9 +316,15 @@ public class DestructionTimeController implements InputParamController, Serializ
 	 * @param timeField						The unit of the interval (<i>null</i> means the job may stay forever).
 	 */
 	public final void setMaxDestructionInterval(int maxDestructionInterval, DateField timeField){
-		this.maxInterval = maxDestructionInterval;
-		maxIntervalField = timeField;
-		maxTime = null;
+		if (maxDestructionInterval <= 0 || timeField == null){
+			this.maxInterval = NO_INTERVAL;
+			maxIntervalField = null;
+			maxTime = null;
+		}else{
+			this.maxInterval = maxDestructionInterval;
+			maxIntervalField = timeField;
+			maxTime = null;
+		}
 	}
 
 	/**
diff --git a/src/uws/job/parameters/ExecutionDurationController.java b/src/uws/job/parameters/ExecutionDurationController.java
index 45cd04b..670d02f 100644
--- a/src/uws/job/parameters/ExecutionDurationController.java
+++ b/src/uws/job/parameters/ExecutionDurationController.java
@@ -31,32 +31,63 @@ import uws.job.UWSJob;
  * 	Moreover you can indicate whether the execution duration of jobs can be modified by the user or not.
  * </p>
  * 
- * <p>
- * 	<i><u>Note:</u>
- * 		By default, the execution duration can be modified by anyone without any limitation.
- * 		The default value is {@link UWSJob#UNLIMITED_DURATION}.
- * 	</i>
- * </p>
+ * <p><i><u>Note:</u> the execution duration is always expressed <b>in seconds</b>.</i></p>
+ * 
+ * <p><i><u>Note:</u>
+ * 	By default, the execution duration can be modified by anyone without any limitation.
+ * 	The default and maximum value is {@link UWSJob#UNLIMITED_DURATION}.
+ * </i></p>
+ * 
+ * <p>The logic of the execution duration is set in this class. Here it is:</p>
+ * <ul>
+ * 	<li>If no value is specified by the UWS client, the default value is returned.</li>
+ *  <li>If no default value is provided, the maximum duration is returned.</li>
+ *  <li>If no maximum value is provided, there is no limit (={@link UWSJob#UNLIMITED_DURATION}).</li>
+ * </ul>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 4.1 (09/2014)
+ * @version 4.1 (11/2014)
  */
 public class ExecutionDurationController implements InputParamController, Serializable {
 	private static final long serialVersionUID = 1L;
 
-	/** The default duration. */
+	/** The default duration (in seconds). */
 	protected long defaultDuration = UWSJob.UNLIMITED_DURATION;
 
-	/** The maximum duration. */
+	/** The maximum duration (in seconds). */
 	protected long maxDuration = UWSJob.UNLIMITED_DURATION;
 
 	/** Indicates whether the execution duration of jobs can be modified. */
 	protected boolean allowModification = true;
 
-	public ExecutionDurationController(){
-		;
-	}
+	/**
+	 * <p>Create a controller for the execution duration.
+	 * By default, there is no maximum value and the default duration is {@link UWSJob#UNLIMITED_DURATION}.</p>
+	 * 
+	 * <p>
+	 * 	A default and/or maximum value can be set after creation using {@link #setDefaultExecutionDuration(long)}
+	 * 	and {@link #setMaxExecutionDuration(long)}. By default this parameter can always be modified, but it can
+	 * 	be forbidden using {@link #allowModification(boolean)}.
+	 * </p>
+	 */
+	public ExecutionDurationController(){}
 
+	/**
+	 * <p>Create a controller for the execution duration.
+	 * The default and the maximum duration are initialized with the given parameters.
+	 * The third parameter allows also to forbid the modification of the execution duration by the user,
+	 * if set to <i>false</i>.</p>
+	 * 
+	 * <p>
+	 * 	A default and/or maximum value can be modified after creation using {@link #setDefaultExecutionDuration(long)}
+	 * 	and {@link #setMaxExecutionDuration(long)}. The flag telling whether this parameter can be modified by the user
+	 * 	can be changed using {@link #allowModification(boolean)}.
+	 * </p>
+	 * 
+	 * @param defaultDuration	Duration (in seconds) set by default to a job, when none is specified.
+	 * @param maxDuration		Maximum duration (in seconds) that can be set. If a greater value is provided by the user, an exception will be thrown by {@link #check(Object)}.
+	 * @param allowModification	<i>true</i> to allow the user to modify this value when creating a job, <i>false</i> otherwise.
+	 */
 	public ExecutionDurationController(final long defaultDuration, final long maxDuration, final boolean allowModification){
 		setDefaultExecutionDuration(defaultDuration);
 		setMaxExecutionDuration(maxDuration);
@@ -65,17 +96,21 @@ public class ExecutionDurationController implements InputParamController, Serial
 
 	@Override
 	public Object getDefault(){
-		return defaultDuration;
+		return (defaultDuration > 0) ? defaultDuration : getMaxExecutionDuration();
 	}
 
 	@Override
-	public Object check(Object value) throws UWSException{
+	public Object check(final Object value) throws UWSException{
+		// If no value, return the default one:
 		if (value == null)
-			return null;
+			return getDefault();
 
+		// Otherwise, parse the given duration:
 		Long duration = null;
 		if (value instanceof Long)
 			duration = (Long)value;
+		else if (value instanceof Integer)
+			duration = (long)(Integer)value;
 		else if (value instanceof String){
 			String strValue = (String)value;
 			try{
@@ -86,10 +121,13 @@ public class ExecutionDurationController implements InputParamController, Serial
 		}else
 			throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Wrong type for the maximum duration parameter: class \"" + value.getClass().getName() + "\"! It should be long or a string containing only a long value.");
 
-		if (duration < UWSJob.UNLIMITED_DURATION)
+		// If the duration is negative or zero, set it to UNLIMITED:
+		if (duration <= 0)
 			duration = UWSJob.UNLIMITED_DURATION;
-		else if (maxDuration > UWSJob.UNLIMITED_DURATION && duration > maxDuration)
-			throw new UWSException(UWSException.BAD_REQUEST, "The UWS limits the execution duration to maximum " + maxDuration + " seconds !");
+
+		// Set the maximum duration if the duration is greater than the maximum value:
+		if (maxDuration > 0 && (duration > maxDuration || duration <= 0))
+			duration = maxDuration;
 
 		return duration;
 	}
@@ -100,16 +138,19 @@ public class ExecutionDurationController implements InputParamController, Serial
 	/**
 	 * Gets the default execution duration.
 	 * 
-	 * @return The default execution duration <i>(0 or less mean an unlimited duration)</i>.
+	 * @return The default execution duration (in seconds) <i>(0 or less mean an unlimited duration)</i>.
+	 * 
+	 * @deprecated This function is completely equivalent to {@link #getDefault()}.
 	 */
+	@Deprecated
 	public final long getDefaultExecutionDuration(){
-		return defaultDuration;
+		return (Long)getDefault();
 	}
 
 	/**
 	 * Sets the default execution duration.
 	 * 
-	 * @param defaultExecutionDuration The new default execution duration <i>({@link UWSJob#UNLIMITED_DURATION}, 0 or a negative value mean an unlimited duration)</i>.
+	 * @param defaultExecutionDuration The new default execution duration (in seconds) <i>({@link UWSJob#UNLIMITED_DURATION}, 0 or a negative value mean an unlimited duration)</i>.
 	 */
 	public final boolean setDefaultExecutionDuration(long defaultExecutionDuration){
 		defaultExecutionDuration = (defaultExecutionDuration <= 0) ? UWSJob.UNLIMITED_DURATION : defaultExecutionDuration;
@@ -125,7 +166,7 @@ public class ExecutionDurationController implements InputParamController, Serial
 	/**
 	 * Gets the maximum execution duration.
 	 * 
-	 * @return The maximum execution duration <i>(0 or less mean an unlimited duration)</i>.
+	 * @return The maximum execution duration (in seconds) <i>(0 or less mean an unlimited duration)</i>.
 	 */
 	public final long getMaxExecutionDuration(){
 		return maxDuration;
@@ -134,7 +175,7 @@ public class ExecutionDurationController implements InputParamController, Serial
 	/**
 	 * Sets the maximum execution duration.
 	 * 
-	 * @param maxExecutionDuration The maximum execution duration <i>({@link UWSJob#UNLIMITED_DURATION}, 0 or a negative value mean an unlimited duration)</i>.
+	 * @param maxExecutionDuration The maximum execution duration (in seconds) <i>({@link UWSJob#UNLIMITED_DURATION}, 0 or a negative value mean an unlimited duration)</i>.
 	 */
 	public final void setMaxExecutionDuration(long maxExecutionDuration){
 		maxDuration = (maxExecutionDuration <= 0) ? UWSJob.UNLIMITED_DURATION : maxExecutionDuration;
diff --git a/src/uws/job/parameters/InputParamController.java b/src/uws/job/parameters/InputParamController.java
index 72e6a33..5cf47ee 100644
--- a/src/uws/job/parameters/InputParamController.java
+++ b/src/uws/job/parameters/InputParamController.java
@@ -25,6 +25,7 @@ import uws.UWSException;
  * <p>Lets controlling an input parameter of a UWS job.</p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS)
+ * @version 4.0
  */
 public interface InputParamController {
 
diff --git a/src/uws/job/parameters/UWSParameters.java b/src/uws/job/parameters/UWSParameters.java
index d41afda..43b9125 100644
--- a/src/uws/job/parameters/UWSParameters.java
+++ b/src/uws/job/parameters/UWSParameters.java
@@ -193,7 +193,6 @@ public class UWSParameters implements Iterable<Entry<String,Object>> {
 	 * 
 	 * @see #UWSParameters(Collection, Map)
 	 */
-	@SuppressWarnings("unchecked")
 	public UWSParameters(final HttpServletRequest request, final Collection<String> expectedAdditionalParams, final Map<String,InputParamController> inputParamControllers) throws UWSException{
 		this(expectedAdditionalParams, inputParamControllers);
 
diff --git a/src/uws/service/UWS.java b/src/uws/service/UWS.java
index 492b17b..a81e296 100644
--- a/src/uws/service/UWS.java
+++ b/src/uws/service/UWS.java
@@ -20,14 +20,10 @@ package uws.service;
  */
 
 import uws.UWSException;
-
 import uws.job.JobList;
-
 import uws.job.serializer.UWSSerializer;
-
 import uws.service.backup.UWSBackupManager;
 import uws.service.file.UWSFileManager;
-
 import uws.service.log.UWSLog;
 
 /**
@@ -183,7 +179,7 @@ public interface UWS extends Iterable<JobList> {
 	/* ******************* */
 
 	/**
-	 * 
+	 * TODO JAVADOC TO WRITE!
 	 * @return
 	 */
 	public UserIdentifier getUserIdentifier();
diff --git a/test/tap/parameters/ServiceConnectionOfTest.java b/test/tap/parameters/ServiceConnectionOfTest.java
new file mode 100644
index 0000000..30a56ee
--- /dev/null
+++ b/test/tap/parameters/ServiceConnectionOfTest.java
@@ -0,0 +1,161 @@
+package tap.parameters;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import tap.ServiceConnection;
+import tap.TAPFactory;
+import tap.TAPJob;
+import tap.file.TAPFileManager;
+import tap.formatter.FITSFormat;
+import tap.formatter.OutputFormat;
+import tap.formatter.SVFormat;
+import tap.formatter.VOTableFormat;
+import tap.log.TAPLog;
+import tap.metadata.TAPMetadata;
+import uws.service.UserIdentifier;
+import adql.db.FunctionDef;
+
+public class ServiceConnectionOfTest implements ServiceConnection {
+
+	private int[] retentionPeriod = new int[]{-1,-1};
+	private int[] executionDuration = new int[]{(int)TAPJob.UNLIMITED_DURATION,(int)TAPJob.UNLIMITED_DURATION};
+	private int[] outputLimit = new int[]{TAPJob.UNLIMITED_MAX_REC,TAPJob.UNLIMITED_MAX_REC};
+	private List<OutputFormat> outputFormats = Arrays.asList(new OutputFormat[]{new VOTableFormat(this),new SVFormat(this, SVFormat.COMMA_SEPARATOR),new FITSFormat(this)});
+
+	@Override
+	public String getProviderName(){
+		return null;
+	}
+
+	@Override
+	public String getProviderDescription(){
+		return null;
+	}
+
+	@Override
+	public boolean isAvailable(){
+		return true;
+	}
+
+	@Override
+	public String getAvailability(){
+		return null;
+	}
+
+	@Override
+	public int[] getRetentionPeriod(){
+		return retentionPeriod;
+	}
+
+	public void setRetentionPeriod(final int defaultVal, final int maxVal){
+		retentionPeriod[0] = defaultVal;
+		retentionPeriod[1] = maxVal;
+	}
+
+	@Override
+	public int[] getExecutionDuration(){
+		return executionDuration;
+	}
+
+	public void setExecutionDuration(final int defaultVal, final int maxVal){
+		executionDuration[0] = defaultVal;
+		executionDuration[1] = maxVal;
+	}
+
+	@Override
+	public int[] getOutputLimit(){
+		return outputLimit;
+	}
+
+	public void setOutputLimit(final int defaultVal, final int maxVal){
+		outputLimit[0] = defaultVal;
+		outputLimit[1] = maxVal;
+	}
+
+	@Override
+	public LimitUnit[] getOutputLimitType(){
+		return new LimitUnit[]{LimitUnit.rows,LimitUnit.rows};
+	}
+
+	@Override
+	public UserIdentifier getUserIdentifier(){
+		return null;
+	}
+
+	@Override
+	public boolean uploadEnabled(){
+		return false;
+	}
+
+	@Override
+	public int[] getUploadLimit(){
+		return null;
+	}
+
+	@Override
+	public LimitUnit[] getUploadLimitType(){
+		return null;
+	}
+
+	@Override
+	public int getMaxUploadSize(){
+		return 0;
+	}
+
+	@Override
+	public TAPMetadata getTAPMetadata(){
+		return null;
+	}
+
+	@Override
+	public Collection<String> getCoordinateSystems(){
+		return null;
+	}
+
+	@Override
+	public Collection<String> getGeometries(){
+		return null;
+	}
+
+	@Override
+	public Collection<FunctionDef> getUDFs(){
+		return null;
+	}
+
+	@Override
+	public int getNbMaxAsyncJobs(){
+		return 0;
+	}
+
+	@Override
+	public TAPLog getLogger(){
+		return null;
+	}
+
+	@Override
+	public TAPFactory getFactory(){
+		return null;
+	}
+
+	@Override
+	public TAPFileManager getFileManager(){
+		return null;
+	}
+
+	@Override
+	public Iterator<OutputFormat> getOutputFormats(){
+		return outputFormats.iterator();
+	}
+
+	@Override
+	public OutputFormat getOutputFormat(String mimeOrAlias){
+		for(OutputFormat f : outputFormats)
+			if (f.getMimeType().equalsIgnoreCase(mimeOrAlias) || f.getShortMimeType().equalsIgnoreCase(mimeOrAlias))
+				return f;
+		return null;
+	}
+
+}
\ No newline at end of file
diff --git a/test/tap/parameters/TestFormatController.java b/test/tap/parameters/TestFormatController.java
new file mode 100644
index 0000000..50d0bb4
--- /dev/null
+++ b/test/tap/parameters/TestFormatController.java
@@ -0,0 +1,89 @@
+package tap.parameters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import tap.TAPJob;
+import uws.UWSException;
+
+public class TestFormatController {
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{}
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@After
+	public void tearDown() throws Exception{}
+
+	@Test
+	public void testCheck(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		FormatController controller = new FormatController(service);
+
+		try{
+			assertEquals(controller.getDefault(), controller.check(null));
+			assertEquals(controller.getDefault(), controller.check(""));
+			assertEquals(controller.getDefault(), controller.check("   "));
+			assertEquals(controller.getDefault(), controller.check("	"));
+			assertEquals(controller.getDefault(), controller.check(" 	 "));
+			assertEquals("votable", controller.check("votable"));
+			assertEquals("application/x-votable+xml", controller.check("application/x-votable+xml"));
+			assertEquals("csv", controller.check("csv"));
+			assertEquals("fits", controller.check("fits"));
+		}catch(Exception ex){
+			ex.printStackTrace();
+			fail();
+		}
+
+		try{
+			controller.check("toto");
+		}catch(Exception ex){
+			assertTrue(ex instanceof UWSException);
+			assertTrue(ex.getMessage().startsWith("Unknown value for the parameter \"format\": \"toto\". It should be "));
+		}
+
+		try{
+			controller.check("application/xml");
+		}catch(Exception ex){
+			assertTrue(ex instanceof UWSException);
+			assertTrue(ex.getMessage().startsWith("Unknown value for the parameter \"format\": \"application/xml\". It should be "));
+		}
+	}
+
+	@Test
+	public void testGetDefault(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		FormatController controller = new FormatController(service);
+
+		assertEquals(TAPJob.FORMAT_VOTABLE, controller.getDefault());
+	}
+
+	@Test
+	public void testAllowModification(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		FormatController controller = new FormatController(service);
+
+		// By default, user modification of the destruction time is allowed:
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(true);
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(false);
+		assertFalse(controller.allowModification());
+	}
+
+}
diff --git a/test/tap/parameters/TestMaxRecController.java b/test/tap/parameters/TestMaxRecController.java
new file mode 100644
index 0000000..613a2f9
--- /dev/null
+++ b/test/tap/parameters/TestMaxRecController.java
@@ -0,0 +1,142 @@
+package tap.parameters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import tap.TAPJob;
+
+public class TestMaxRecController {
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{}
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@After
+	public void tearDown() throws Exception{}
+
+	@Test
+	public void testCheck(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		MaxRecController controller = new MaxRecController(service);
+
+		try{
+			// A NULL limit will always return an unlimited duration:
+			assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(null));
+			assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(-1));
+			assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(-123));
+
+			// A 0 value, means that only the metadata of the result must be returned (without executing the query);
+			// this value should stay like that:
+			assertEquals(0, controller.check(0));
+
+			// By default, the controller has no limit on the output limit, so let's try with a limit of 1000000 rows:
+			assertEquals(1000000, controller.check(1000000));
+
+			// With just a default output limit (of 100 rows):
+			service.setOutputLimit(100, -1);
+			assertEquals(100, controller.check(null));
+			assertEquals(0, controller.check(0));
+			assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(-1));
+			assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.check(TAPJob.UNLIMITED_MAX_REC));
+
+			// With just a maximum output limit (of 10000 rows):
+			service.setOutputLimit(-1, 10000);
+			assertEquals(10000, controller.check(null));
+			assertEquals(0, controller.check(0));
+			assertEquals(60, controller.check(60));
+			assertEquals(10000, controller.check(-1));
+			assertEquals(10000, controller.check(TAPJob.UNLIMITED_MAX_REC));
+			assertEquals(10000, controller.check(10001));
+
+			// With a default (100 rows) AND a maximum (10000 rows) output limit:
+			service.setOutputLimit(100, 10000);
+			assertEquals(100, controller.check(null));
+			assertEquals(0, controller.check(0));
+			assertEquals(10, controller.check(10));
+			assertEquals(600, controller.check(600));
+			assertEquals(10000, controller.check(10000));
+			assertEquals(10000, controller.check(-1));
+			assertEquals(10000, controller.check(TAPJob.UNLIMITED_MAX_REC));
+			assertEquals(10000, controller.check(10001));
+
+		}catch(Exception t){
+			t.printStackTrace();
+			fail();
+		}
+	}
+
+	@Test
+	public void testGetDefault(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		MaxRecController controller = new MaxRecController(service);
+
+		// By default, when nothing is set, the default output limit is UNLIMITED:
+		assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getDefault());
+
+		// With no duration, the default output limit should remain UNLIMITED:
+		service.setOutputLimit(TAPJob.UNLIMITED_MAX_REC, -1);
+		assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getDefault());
+
+		// With a negative limit, the output limit should also be UNLIMITED:
+		service.setOutputLimit(-1, -1);
+		assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getDefault());
+
+		// With an output limit of 100 rows:
+		service.setOutputLimit(100, -1);
+		assertEquals(100, controller.getDefault());
+
+		// The default value must always be less than the maximum value:
+		service.setOutputLimit(600, 300);
+		assertEquals(300, controller.getDefault());
+	}
+
+	@Test
+	public void testGetMaxExecutionDuration(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		MaxRecController controller = new MaxRecController(service);
+
+		// By default, when nothing is set, the maximum output limit is UNLIMITED:
+		assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getMaxOutputLimit());
+
+		// With no duration, the maximum output limit should remain UNLIMITED:
+		service.setOutputLimit(-1, TAPJob.UNLIMITED_MAX_REC);
+		assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getMaxOutputLimit());
+
+		// With a negative limit, the output limit should also be UNLIMITED:
+		service.setOutputLimit(-1, -1);
+		assertEquals(TAPJob.UNLIMITED_MAX_REC, controller.getMaxOutputLimit());
+
+		// With an output limit of 10000 rows:
+		service.setOutputLimit(-1, 10000);
+		assertEquals(10000, controller.getMaxOutputLimit());
+	}
+
+	@Test
+	public void testAllowModification(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		MaxRecController controller = new MaxRecController(service);
+
+		// By default, user modification of the destruction time is allowed:
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(true);
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(false);
+		assertFalse(controller.allowModification());
+	}
+
+}
diff --git a/test/tap/parameters/TestTAPDestructionTimeController.java b/test/tap/parameters/TestTAPDestructionTimeController.java
new file mode 100644
index 0000000..73453af
--- /dev/null
+++ b/test/tap/parameters/TestTAPDestructionTimeController.java
@@ -0,0 +1,187 @@
+package tap.parameters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import uws.ISO8601Format;
+
+public class TestTAPDestructionTimeController {
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{}
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@After
+	public void tearDown() throws Exception{}
+
+	@Test
+	public void testCheck(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		TAPDestructionTimeController controller = new TAPDestructionTimeController(service);
+		Calendar calendar = Calendar.getInstance();
+
+		try{
+			// A NULL destruction time will always return NULL:
+			assertNull(controller.check(null));
+
+			// By default, the controller has no limit on the destruction time, so let's try with a destruction in 100 years:
+			calendar.add(Calendar.YEAR, 100);
+			checkDate(calendar.getTime(), controller.check(calendar.getTime()));
+			checkDate(calendar.getTime(), controller.check(ISO8601Format.format(calendar.getTimeInMillis())));
+
+			// With just a default destruction time (of 10 minutes):
+			service.setRetentionPeriod(600, -1);
+			Calendar defaultTime = Calendar.getInstance();
+			defaultTime.add(Calendar.MINUTE, 10);
+			checkDate(defaultTime.getTime(), controller.check(null));
+			checkDate(calendar.getTime(), controller.check(calendar.getTime()));
+
+			// With just a maximum destruction time (of 1 hour):
+			service.setRetentionPeriod(0, 3600);
+			Calendar maxTime = Calendar.getInstance();
+			maxTime.add(Calendar.HOUR, 1);
+			checkDate(maxTime.getTime(), controller.check(null));
+			checkDate(defaultTime.getTime(), controller.check(defaultTime.getTime()));
+			checkDate(maxTime.getTime(), controller.check(calendar.getTime()));
+
+			// With a default (10 minutes) AND a maximum (1 hour) destruction time:
+			service.setRetentionPeriod(600, 3600);
+			checkDate(defaultTime.getTime(), controller.check(null));
+			checkDate(maxTime.getTime(), controller.check(calendar.getTime()));
+			calendar = Calendar.getInstance();
+			calendar.add(Calendar.MINUTE, 30);
+			checkDate(calendar.getTime(), controller.check(calendar.getTime()));
+
+		}catch(Exception t){
+			t.printStackTrace();
+			fail();
+		}
+	}
+
+	@Test
+	public void testGetDefault(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		TAPDestructionTimeController controller = new TAPDestructionTimeController(service);
+
+		// By default, when nothing is set, the default destruction time is NULL (the job will never be destroyed):
+		assertNull(controller.getDefault());
+
+		// With no interval, the default destruction time should remain NULL (the job will never be destroyed):
+		service.setRetentionPeriod(0, -1);
+		assertNull(controller.getDefault());
+
+		// With a negative interval, the destruction time should also be NULL:
+		service.setRetentionPeriod(-1, -1);
+		assertNull(controller.getDefault());
+
+		// With a destruction interval of 100 minutes:
+		Calendar calendar = Calendar.getInstance();
+		service.setRetentionPeriod(6000, -1);
+		calendar.add(Calendar.MINUTE, 100);
+		checkDate(calendar.getTime(), controller.getDefault());
+
+		// With a destruction interval of 100 seconds:
+		service.setRetentionPeriod(100, -1);
+		calendar = Calendar.getInstance();
+		calendar.add(Calendar.SECOND, 100);
+		checkDate(calendar.getTime(), controller.getDefault());
+
+		// With a destruction interval of 1 week:
+		service.setRetentionPeriod(7 * 24 * 3600, -1);
+		calendar = Calendar.getInstance();
+		calendar.add(Calendar.DAY_OF_MONTH, 7);
+		checkDate(calendar.getTime(), controller.getDefault());
+	}
+
+	@Test
+	public void testGetMaxDestructionTime(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		TAPDestructionTimeController controller = new TAPDestructionTimeController(service);
+
+		// By default, when nothing is set, the maximum destruction time is NULL (the job will never be destroyed):
+		assertNull(controller.getMaxDestructionTime());
+
+		// With no interval, the maximum destruction time should remain NULL (the job will never be destroyed):
+		service.setRetentionPeriod(-1, 0);
+		assertNull(controller.getMaxDestructionTime());
+
+		// With a negative interval, the destruction time should also be NULL:
+		service.setRetentionPeriod(-1, -1);
+		assertNull(controller.getMaxDestructionTime());
+
+		// With a destruction interval of 100 minutes:
+		Calendar calendar = Calendar.getInstance();
+		service.setRetentionPeriod(-1, 6000);
+		calendar.add(Calendar.MINUTE, 100);
+		checkDate(calendar.getTime(), controller.getMaxDestructionTime());
+
+		// With a destruction interval of 100 seconds:
+		service.setRetentionPeriod(-1, 100);
+		calendar = Calendar.getInstance();
+		calendar.add(Calendar.SECOND, 100);
+		checkDate(calendar.getTime(), controller.getMaxDestructionTime());
+
+		// With a destruction interval of 1 week:
+		service.setRetentionPeriod(-1, 7 * 24 * 3600);
+		calendar = Calendar.getInstance();
+		calendar.add(Calendar.DAY_OF_MONTH, 7);
+		checkDate(calendar.getTime(), controller.getMaxDestructionTime());
+	}
+
+	@Test
+	public void testAllowModification(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		TAPDestructionTimeController controller = new TAPDestructionTimeController(service);
+
+		// By default, user modification of the destruction time is allowed:
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(true);
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(false);
+		assertFalse(controller.allowModification());
+	}
+
+	private void checkDate(final Date expected, final Object val){
+		assertTrue(val instanceof Date);
+
+		if (expected != null && val != null){
+			Calendar cexpected = Calendar.getInstance(), cval = Calendar.getInstance();
+			cexpected.setTime(expected);
+			cval.setTime((Date)val);
+
+			try{
+				assertEquals(cexpected.get(Calendar.DAY_OF_MONTH), cval.get(Calendar.DAY_OF_MONTH));
+				assertEquals(cexpected.get(Calendar.MONTH), cval.get(Calendar.MONTH));
+				assertEquals(cexpected.get(Calendar.YEAR), cval.get(Calendar.YEAR));
+				assertEquals(cexpected.get(Calendar.HOUR), cval.get(Calendar.HOUR));
+				assertEquals(cexpected.get(Calendar.MINUTE), cval.get(Calendar.MINUTE));
+				assertEquals(cexpected.get(Calendar.SECOND), cval.get(Calendar.SECOND));
+			}catch(AssertionError e){
+				fail("Expected <" + expected + "> but was <" + val + ">");
+			}
+		}else if (expected == null && val == null)
+			return;
+		else
+			fail("Expected <" + expected + "> but was <" + val + ">");
+	}
+
+}
diff --git a/test/tap/parameters/TestTAPExecutionDurationController.java b/test/tap/parameters/TestTAPExecutionDurationController.java
new file mode 100644
index 0000000..446c58e
--- /dev/null
+++ b/test/tap/parameters/TestTAPExecutionDurationController.java
@@ -0,0 +1,136 @@
+package tap.parameters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import tap.TAPJob;
+
+public class TestTAPExecutionDurationController {
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{}
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@After
+	public void tearDown() throws Exception{}
+
+	@Test
+	public void testCheck(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		TAPExecutionDurationController controller = new TAPExecutionDurationController(service);
+
+		try{
+			// A NULL duration will always return an unlimited duration:
+			assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(null));
+			assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(0));
+			assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(-1));
+			assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(-123));
+
+			// By default, the controller has no limit on the execution duration, so let's try with a duration of 1e6 seconds:
+			assertEquals(1000000L, controller.check(1000000));
+
+			// With just a default execution duration (of 10 minutes):
+			service.setExecutionDuration(600, -1);
+			assertEquals(600L, controller.check(null));
+			assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(-1));
+			assertEquals(TAPJob.UNLIMITED_DURATION, controller.check(TAPJob.UNLIMITED_DURATION));
+
+			// With just a maximum execution duration (of 1 hour):
+			service.setExecutionDuration(-1, 3600);
+			assertEquals(3600L, controller.check(null));
+			assertEquals(60L, controller.check(60));
+			assertEquals(3600L, controller.check(-1));
+			assertEquals(3600L, controller.check(TAPJob.UNLIMITED_DURATION));
+			assertEquals(3600L, controller.check(3601));
+
+			// With a default (10 minutes) AND a maximum (1 hour) execution duration:
+			service.setExecutionDuration(600, 3600);
+			assertEquals(600L, controller.check(null));
+			assertEquals(10L, controller.check(10));
+			assertEquals(600L, controller.check(600));
+			assertEquals(3600L, controller.check(3600));
+			assertEquals(3600L, controller.check(-1));
+			assertEquals(3600L, controller.check(TAPJob.UNLIMITED_DURATION));
+			assertEquals(3600L, controller.check(3601));
+
+		}catch(Exception t){
+			t.printStackTrace();
+			fail();
+		}
+	}
+
+	@Test
+	public void testGetDefault(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		TAPExecutionDurationController controller = new TAPExecutionDurationController(service);
+
+		// By default, when nothing is set, the default execution duration is UNLIMITED:
+		assertEquals(TAPJob.UNLIMITED_DURATION, controller.getDefault());
+
+		// With no duration, the default execution duration should remain UNLIMITED:
+		service.setExecutionDuration((int)TAPJob.UNLIMITED_DURATION, -1);
+		assertEquals(TAPJob.UNLIMITED_DURATION, controller.getDefault());
+
+		// With a negative duration, the execution duration should also be UNLIMITED:
+		service.setExecutionDuration(-1, -1);
+		assertEquals(TAPJob.UNLIMITED_DURATION, controller.getDefault());
+
+		// With an execution duration of 10 minutes:
+		service.setExecutionDuration(600, -1);
+		assertEquals(600L, controller.getDefault());
+
+		// The default value must always be less than the maximum value:
+		service.setExecutionDuration(600, 300);
+		assertEquals(300L, controller.getDefault());
+	}
+
+	@Test
+	public void testGetMaxExecutionDuration(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		TAPExecutionDurationController controller = new TAPExecutionDurationController(service);
+
+		// By default, when nothing is set, the maximum execution duration is UNLIMITED:
+		assertEquals(TAPJob.UNLIMITED_DURATION, controller.getMaxDuration());
+
+		// With no duration, the maximum execution duration should remain UNLIMITED:
+		service.setExecutionDuration(-1, (int)TAPJob.UNLIMITED_DURATION);
+		assertEquals(TAPJob.UNLIMITED_DURATION, controller.getMaxDuration());
+
+		// With a negative duration, the execution duration should also be UNLIMITED:
+		service.setExecutionDuration(-1, -1);
+		assertEquals(TAPJob.UNLIMITED_DURATION, controller.getMaxDuration());
+
+		// With an execution duration of 10 minutes:
+		service.setExecutionDuration(-1, 600);
+		assertEquals(600L, controller.getMaxDuration());
+	}
+
+	@Test
+	public void testAllowModification(){
+		ServiceConnectionOfTest service = new ServiceConnectionOfTest();
+		TAPExecutionDurationController controller = new TAPExecutionDurationController(service);
+
+		// By default, user modification of the execution duration is allowed:
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(true);
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(false);
+		assertFalse(controller.allowModification());
+	}
+
+}
diff --git a/test/uws/job/parameters/TestDestructionTimeController.java b/test/uws/job/parameters/TestDestructionTimeController.java
new file mode 100644
index 0000000..a50d5eb
--- /dev/null
+++ b/test/uws/job/parameters/TestDestructionTimeController.java
@@ -0,0 +1,186 @@
+package uws.job.parameters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import uws.ISO8601Format;
+import uws.job.parameters.DestructionTimeController.DateField;
+
+public class TestDestructionTimeController {
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{}
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@After
+	public void tearDown() throws Exception{}
+
+	@Test
+	public void testCheck(){
+		DestructionTimeController controller = new DestructionTimeController();
+		Calendar calendar = Calendar.getInstance();
+
+		try{
+			// A NULL destruction time will always return NULL:
+			assertNull(controller.check(null));
+
+			// By default, the controller has no limit on the destruction time, so let's try with a destruction in 100 years:
+			calendar.add(Calendar.YEAR, 100);
+			checkDate(calendar.getTime(), controller.check(calendar.getTime()));
+			checkDate(calendar.getTime(), controller.check(ISO8601Format.format(calendar.getTimeInMillis())));
+
+			// With just a default destruction time (of 10 minutes):
+			controller.setDefaultDestructionInterval(10);
+			Calendar defaultTime = Calendar.getInstance();
+			defaultTime.add(Calendar.MINUTE, 10);
+			checkDate(defaultTime.getTime(), controller.check(null));
+			checkDate(calendar.getTime(), controller.check(calendar.getTime()));
+
+			// With just a maximum destruction time (of 1 hour):
+			controller.setDefaultDestructionInterval(0);
+			controller.setMaxDestructionInterval(1, DateField.HOUR);
+			Calendar maxTime = Calendar.getInstance();
+			maxTime.add(Calendar.HOUR, 1);
+			checkDate(maxTime.getTime(), controller.check(null));
+			checkDate(defaultTime.getTime(), controller.check(defaultTime.getTime()));
+			checkDate(maxTime.getTime(), controller.check(calendar.getTime()));
+
+			// With a default (10 minutes) AND a maximum (1 hour) destruction time:
+			controller.setDefaultDestructionInterval(10);
+			controller.setMaxDestructionInterval(1, DateField.HOUR);
+			checkDate(defaultTime.getTime(), controller.check(null));
+			checkDate(maxTime.getTime(), controller.check(calendar.getTime()));
+			calendar = Calendar.getInstance();
+			calendar.add(Calendar.MINUTE, 30);
+			checkDate(calendar.getTime(), controller.check(calendar.getTime()));
+
+		}catch(Exception t){
+			t.printStackTrace();
+			fail();
+		}
+	}
+
+	@Test
+	public void testGetDefault(){
+		DestructionTimeController controller = new DestructionTimeController();
+
+		// By default, when nothing is set, the default destruction time is NULL (the job will never be destroyed):
+		assertNull(controller.getDefault());
+
+		// With no interval, the default destruction time should remain NULL (the job will never be destroyed):
+		controller.setDefaultDestructionInterval(DestructionTimeController.NO_INTERVAL);
+		assertNull(controller.getDefault());
+
+		// With a negative interval, the destruction time should also be NULL:
+		controller.setDefaultDestructionInterval(-1);
+		assertNull(controller.getDefault());
+
+		// With a destruction interval of 100 minutes:
+		Calendar calendar = Calendar.getInstance();
+		controller.setDefaultDestructionInterval(100);
+		calendar.add(Calendar.MINUTE, 100);
+		checkDate(calendar.getTime(), controller.getDefault());
+
+		// With a destruction interval of 100 seconds:
+		controller.setDefaultDestructionInterval(100, DateField.SECOND);
+		calendar = Calendar.getInstance();
+		calendar.add(Calendar.SECOND, 100);
+		checkDate(calendar.getTime(), controller.getDefault());
+
+		// With a destruction interval of 1 week:
+		controller.setDefaultDestructionInterval(7, DateField.DAY);
+		calendar = Calendar.getInstance();
+		calendar.add(Calendar.DAY_OF_MONTH, 7);
+		checkDate(calendar.getTime(), controller.getDefault());
+	}
+
+	@Test
+	public void testGetMaxDestructionTime(){
+		DestructionTimeController controller = new DestructionTimeController();
+
+		// By default, when nothing is set, the maximum destruction time is NULL (the job will never be destroyed):
+		assertNull(controller.getMaxDestructionTime());
+
+		// With no interval, the maximum destruction time should remain NULL (the job will never be destroyed):
+		controller.setMaxDestructionInterval(DestructionTimeController.NO_INTERVAL);
+		assertNull(controller.getMaxDestructionTime());
+
+		// With a negative interval, the destruction time should also be NULL:
+		controller.setMaxDestructionInterval(-1);
+		assertNull(controller.getMaxDestructionTime());
+
+		// With a destruction interval of 100 minutes:
+		Calendar calendar = Calendar.getInstance();
+		controller.setMaxDestructionInterval(100);
+		calendar.add(Calendar.MINUTE, 100);
+		checkDate(calendar.getTime(), controller.getMaxDestructionTime());
+
+		// With a destruction interval of 100 seconds:
+		controller.setMaxDestructionInterval(100, DateField.SECOND);
+		calendar = Calendar.getInstance();
+		calendar.add(Calendar.SECOND, 100);
+		checkDate(calendar.getTime(), controller.getMaxDestructionTime());
+
+		// With a destruction interval of 1 week:
+		controller.setMaxDestructionInterval(7, DateField.DAY);
+		calendar = Calendar.getInstance();
+		calendar.add(Calendar.DAY_OF_MONTH, 7);
+		checkDate(calendar.getTime(), controller.getMaxDestructionTime());
+	}
+
+	@Test
+	public void testAllowModification(){
+		DestructionTimeController controller = new DestructionTimeController();
+
+		// By default, user modification of the destruction time is allowed:
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(true);
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(false);
+		assertFalse(controller.allowModification());
+	}
+
+	private void checkDate(final Date expected, final Object val){
+		assertTrue(val instanceof Date);
+
+		if (expected != null && val != null){
+			Calendar cexpected = Calendar.getInstance(), cval = Calendar.getInstance();
+			cexpected.setTime(expected);
+			cval.setTime((Date)val);
+
+			try{
+				assertEquals(cexpected.get(Calendar.DAY_OF_MONTH), cval.get(Calendar.DAY_OF_MONTH));
+				assertEquals(cexpected.get(Calendar.MONTH), cval.get(Calendar.MONTH));
+				assertEquals(cexpected.get(Calendar.YEAR), cval.get(Calendar.YEAR));
+				assertEquals(cexpected.get(Calendar.HOUR), cval.get(Calendar.HOUR));
+				assertEquals(cexpected.get(Calendar.MINUTE), cval.get(Calendar.MINUTE));
+				assertEquals(cexpected.get(Calendar.SECOND), cval.get(Calendar.SECOND));
+			}catch(AssertionError e){
+				fail("Expected <" + expected + "> but was <" + val + ">");
+			}
+		}else if (expected == null && val == null)
+			return;
+		else
+			fail("Expected <" + expected + "> but was <" + val + ">");
+	}
+
+}
diff --git a/test/uws/job/parameters/TestExecutionDurationController.java b/test/uws/job/parameters/TestExecutionDurationController.java
new file mode 100644
index 0000000..65eef00
--- /dev/null
+++ b/test/uws/job/parameters/TestExecutionDurationController.java
@@ -0,0 +1,134 @@
+package uws.job.parameters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import uws.job.UWSJob;
+
+public class TestExecutionDurationController {
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{}
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@After
+	public void tearDown() throws Exception{}
+
+	@Test
+	public void testCheck(){
+		ExecutionDurationController controller = new ExecutionDurationController();
+
+		try{
+			// A NULL duration will always return an unlimited duration:
+			assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(null));
+			assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(0));
+			assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(-1));
+			assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(-123));
+
+			// By default, the controller has no limit on the execution duration, so let's try with a duration of 1e6 seconds:
+			assertEquals(1000000L, controller.check(1000000));
+
+			// With just a default execution duration (of 10 minutes):
+			controller.setDefaultExecutionDuration(600);
+			assertEquals(600L, controller.check(null));
+			assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(-1));
+			assertEquals(UWSJob.UNLIMITED_DURATION, controller.check(UWSJob.UNLIMITED_DURATION));
+
+			// With just a maximum execution duration (of 1 hour):
+			controller.setDefaultExecutionDuration(-1);
+			controller.setMaxExecutionDuration(3600);
+			assertEquals(3600L, controller.check(null));
+			assertEquals(60L, controller.check(60));
+			assertEquals(3600L, controller.check(-1));
+			assertEquals(3600L, controller.check(UWSJob.UNLIMITED_DURATION));
+			assertEquals(3600L, controller.check(3601));
+
+			// With a default (10 minutes) AND a maximum (1 hour) execution duration:
+			controller.setDefaultExecutionDuration(600);
+			controller.setMaxExecutionDuration(3600);
+			assertEquals(600L, controller.check(null));
+			assertEquals(10L, controller.check(10));
+			assertEquals(600L, controller.check(600));
+			assertEquals(3600L, controller.check(3600));
+			assertEquals(3600L, controller.check(-1));
+			assertEquals(3600L, controller.check(UWSJob.UNLIMITED_DURATION));
+			assertEquals(3600L, controller.check(3601));
+
+		}catch(Exception t){
+			t.printStackTrace();
+			fail();
+		}
+	}
+
+	@Test
+	public void testGetDefault(){
+		ExecutionDurationController controller = new ExecutionDurationController();
+
+		// By default, when nothing is set, the default execution duration is UNLIMITED:
+		assertEquals(UWSJob.UNLIMITED_DURATION, controller.getDefault());
+
+		// With no duration, the default execution duration should remain UNLIMITED:
+		controller.setDefaultExecutionDuration(UWSJob.UNLIMITED_DURATION);
+		assertEquals(UWSJob.UNLIMITED_DURATION, controller.getDefault());
+
+		// With a negative duration, the execution duration should also be UNLIMITED:
+		controller.setDefaultExecutionDuration(-1);
+		assertEquals(UWSJob.UNLIMITED_DURATION, controller.getDefault());
+
+		// With an execution duration of 10 minutes:
+		controller.setDefaultExecutionDuration(600);
+		assertEquals(600L, controller.getDefault());
+
+		// The default value must always be less than the maximum value:
+		controller.setMaxExecutionDuration(300);
+		assertEquals(300L, controller.getDefault());
+	}
+
+	@Test
+	public void testGetMaxExecutionDuration(){
+		ExecutionDurationController controller = new ExecutionDurationController();
+
+		// By default, when nothing is set, the maximum execution duration is UNLIMITED:
+		assertEquals(UWSJob.UNLIMITED_DURATION, controller.getMaxExecutionDuration());
+
+		// With no duration, the maximum execution duration should remain UNLIMITED:
+		controller.setMaxExecutionDuration(UWSJob.UNLIMITED_DURATION);
+		assertEquals(UWSJob.UNLIMITED_DURATION, controller.getMaxExecutionDuration());
+
+		// With a negative duration, the execution duration should also be UNLIMITED:
+		controller.setMaxExecutionDuration(-1);
+		assertEquals(UWSJob.UNLIMITED_DURATION, controller.getMaxExecutionDuration());
+
+		// With an execution duration of 10 minutes:
+		controller.setMaxExecutionDuration(600);
+		assertEquals(600L, controller.getMaxExecutionDuration());
+	}
+
+	@Test
+	public void testAllowModification(){
+		ExecutionDurationController controller = new ExecutionDurationController();
+
+		// By default, user modification of the execution duration is allowed:
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(true);
+		assertTrue(controller.allowModification());
+
+		controller.allowModification(false);
+		assertFalse(controller.allowModification());
+	}
+
+}
-- 
GitLab