From d2e5d98a2213a705472f260b08ba05d385369cbe Mon Sep 17 00:00:00 2001
From: gmantele <gmantele@ari.uni-heidelberg.de>
Date: Fri, 3 Nov 2017 17:47:57 +0100
Subject: [PATCH] [UWS] Support the blocking behavior described in PR-UWS-1.1.

It is possible to choose how the blocking mechanism should behave
(e.g. what the max. waiting period, how many requests can be blocked
in the same time, what happen when the blocking times out, ...).

Indeed, the policy to apply must actually be an extension of the interface
BlockingPolicy. Already two implementations are provided in the library
(LimitedBlockingPolicy and UserLimitedBlockingPolicy), but a custom policy
can perfectly be created and apply to a UWS service.

By default, no policy is set. In such case, the service will block the time
specified by the user, which may be -1 (i.e. wait indefinitely). A
BlockingPolicy can help controlling the waiting/blocking process and protect
the resources of the server.
---
 src/uws/service/UWSService.java               | 291 +++++---
 src/uws/service/UWSServlet.java               |  64 +-
 src/uws/service/actions/JobSummary.java       | 227 +++++-
 src/uws/service/wait/BlockingPolicy.java      | 114 +++
 .../service/wait/LimitedBlockingPolicy.java   |  89 +++
 .../wait/UserLimitedBlockingPolicy.java       | 311 ++++++++
 src/uws/service/wait/WaitObserver.java        |  60 ++
 test/uws/service/actions/TestJobSummary.java  | 645 ++++++++++++++++
 .../wait/TestLimitedBlockingPolicy.java       |  67 ++
 .../wait/TestUserLimitedBlockingPolicy.java   | 706 ++++++++++++++++++
 test/uws/service/wait/TestWaitObserver.java   |  77 ++
 11 files changed, 2512 insertions(+), 139 deletions(-)
 create mode 100644 src/uws/service/wait/BlockingPolicy.java
 create mode 100644 src/uws/service/wait/LimitedBlockingPolicy.java
 create mode 100644 src/uws/service/wait/UserLimitedBlockingPolicy.java
 create mode 100644 src/uws/service/wait/WaitObserver.java
 create mode 100644 test/uws/service/actions/TestJobSummary.java
 create mode 100644 test/uws/service/wait/TestLimitedBlockingPolicy.java
 create mode 100644 test/uws/service/wait/TestUserLimitedBlockingPolicy.java
 create mode 100644 test/uws/service/wait/TestWaitObserver.java

diff --git a/src/uws/service/UWSService.java b/src/uws/service/UWSService.java
index c35cad9..600cf02 100644
--- a/src/uws/service/UWSService.java
+++ b/src/uws/service/UWSService.java
@@ -2,20 +2,20 @@ package uws.service;
 
 /*
  * This file is part of UWSLibrary.
- * 
+ *
  * UWSLibrary is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published by
  * the Free Software Foundation, either version 3 of the License, or
  * (at your option) any later version.
- * 
+ *
  * UWSLibrary is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU Lesser General Public License for more details.
- * 
+ *
  * 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-2017 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
  *                       Astronomisches Rechen Institut (ARI)
  */
@@ -60,12 +60,13 @@ import uws.service.log.DefaultUWSLog;
 import uws.service.log.UWSLog;
 import uws.service.log.UWSLog.LogLevel;
 import uws.service.request.RequestParser;
+import uws.service.wait.BlockingPolicy;
 
 /**
  * <p>This class implements directly the interface {@link UWS} and so, it represents the core of a UWS service.</p>
- * 
+ *
  * <h3>Usage</h3>
- * 
+ *
  * <p>
  * 	Using this class is very simple! An instance must be created by providing at a factory - {@link UWSFactory} - and a file manager - {@link UWSFileManager}.
  * 	This creation must be done in the init() function of a {@link HttpServlet}. Then, still in init(), at least one job list must be created.
@@ -76,7 +77,7 @@ import uws.service.request.RequestParser;
  * <pre>
  * public class MyUWSService extends HttpServlet {
  * 	private UWS uws;
- * 
+ *
  * 	public void init(ServletConfig config) throws ServletException {
  * 		try{
  * 			// Create the UWS service:
@@ -87,12 +88,12 @@ import uws.service.request.RequestParser;
  * 			throw new ServletException("Can not initialize the UWS service!", ue);
  * 		}
  * 	}
- * 
+ *
  * 	public void destroy(){
  *		if (uws != null)
  * 			uws.destroy();
  * 	}
- * 
+ *
  * 	public void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException{
  * 		try{
  *			service.executeRequest(request, response);
@@ -102,9 +103,9 @@ import uws.service.request.RequestParser;
  * 	}
  * }
  * </pre>
- * 
+ *
  * <h3>UWS actions</h3>
- * 
+ *
  * <p>
  * 	All standard UWS actions are already implemented in this class. However, it is still possible to modify their implementation and/or to
  * 	add or remove some actions.
@@ -127,18 +128,18 @@ import uws.service.request.RequestParser;
  * 	name representing the action. Thus, it is possible to replace a UWS action implementation by using the function {@link #replaceUWSAction(UWSAction)} ; this
  * 	function will replace the action having the same name as the given action.
  * </p>
- * 
+ *
  * <h3>Home page</h3>
- * 
+ *
  * <p>
  * 	In addition of all the actions listed above, a last action is automatically added: {@link ShowHomePage}. This is the action which will display the home page of
  * 	the UWS service. It is called when the root resource of the web service is asked. To change it, you can either overwrite this action
  * 	(see {@link #replaceUWSAction(UWSAction)}) or set an home page URL with the function {@link #setHomePage(String)} <i>(the parameter is a URI pointing on either
  * 	a local or a remote resource)</i> or {@link #setHomePage(URL, boolean)}.
  * </p>
- * 
+ *
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 4.2 (09/2017)
+ * @version 4.3 (11/2017)
  */
 public class UWSService implements UWS {
 
@@ -201,6 +202,16 @@ public class UWSService implements UWS {
 	/** Lets writing/formatting any exception/throwable in a HttpServletResponse. */
 	protected ServiceErrorWriter errorWriter;
 
+	/**
+	 * Strategy to use for the blocking/wait process concerning the
+	 * {@link JobSummary} action.
+	 * <p>
+	 * 	If NULL, the standard strategy will be used: wait exactly the time asked
+	 * 	by the user (or indefinitely if none is specified).
+	 * </p>
+	 * @since 4.3 */
+	protected BlockingPolicy waitPolicy = null;
+
 	/** Last generated request ID. If the next generated request ID is equivalent to this one,
 	 * a new one will generate in order to ensure the unicity.
 	 * @since 4.1 */
@@ -211,21 +222,21 @@ public class UWSService implements UWS {
 	/* ************ */
 	/**
 	 * <p>Builds a UWS (the base URI will be extracted at the first request directly from the request itself).</p>
-	 * 
+	 *
 	 * <p>
 	 * 	By default, this UWS has 2 serialization formats: XML ({@link XMLSerializer}) and JSON ({@link JSONSerializer}).
 	 * 	All the default actions of a UWS are also already implemented.
 	 * 	However, you still have to create at least one job list !
 	 * </p>
-	 * 
+	 *
 	 * <p><i><u>note:</u> since no logger is provided, a default one is set automatically (see {@link DefaultUWSLog}).</i></p>
-	 * 
+	 *
 	 * @param jobFactory	Object which lets creating the UWS jobs managed by this UWS and their thread/task.
 	 * @param fileManager	Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...).
-	 * 
+	 *
 	 * @throws NullPointerException	If at least one of the parameters is <i>null</i>.
 	 * @throws UWSException			If unable to create a request parser using the factory (see {@link UWSFactory#createRequestParser(UWSFileManager)}).
-	 * 
+	 *
 	 * @see #UWSService(UWSFactory, UWSFileManager, UWSLog)
 	 */
 	public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager) throws UWSException{
@@ -234,17 +245,17 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Builds a UWS (the base URI will be extracted at the first request directly from the request itself).</p>
-	 * 
+	 *
 	 * <p>
 	 * 	By default, this UWS has 2 serialization formats: XML ({@link XMLSerializer}) and JSON ({@link JSONSerializer}).
 	 * 	All the default actions of a UWS are also already implemented.
 	 * 	However, you still have to create at least one job list !
 	 * </p>
-	 * 
+	 *
 	 * @param jobFactory	Object which lets creating the UWS jobs managed by this UWS and their thread/task.
 	 * @param fileManager	Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...).
 	 * @param logger		Object which lets printing any message (error, info, debug, warning).
-	 * 
+	 *
 	 * @throws NullPointerException	If at least one of the parameters is <i>null</i>.
 	 * @throws UWSException			If unable to create a request parser using the factory (see {@link UWSFactory#createRequestParser(UWSFileManager)}).
 	 */
@@ -287,15 +298,15 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Builds a UWS with its base UWS URI.</p>
-	 * 
+	 *
 	 * <p><i><u>note:</u> since no logger is provided, a default one is set automatically (see {@link DefaultUWSLog}).</i></p>
-	 * 
+	 *
 	 * @param jobFactory	Object which lets creating the UWS jobs managed by this UWS and their thread/task.
 	 * @param fileManager	Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...).
 	 * @param baseURI		Base UWS URI.
-	 * 
+	 *
 	 * @throws UWSException	If the given URI is <i>null</i> or empty.
-	 * 
+	 *
 	 * @see #UWSService(UWSFactory, UWSFileManager, UWSLog, String)
 	 */
 	public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final String baseURI) throws UWSException{
@@ -304,14 +315,14 @@ public class UWSService implements UWS {
 
 	/**
 	 * Builds a UWS with its base UWS URI.
-	 * 
+	 *
 	 * @param jobFactory	Object which lets creating the UWS jobs managed by this UWS and their thread/task.
 	 * @param fileManager	Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...).
 	 * @param logger		Object which lets printing any message (error, info, debug, warning).
 	 * @param baseURI		Base UWS URI.
-	 * 
+	 *
 	 * @throws UWSException	If the given URI is <i>null</i> or empty.
-	 * 
+	 *
 	 * @see UWSUrl#UWSUrl(String)
 	 */
 	public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger, final String baseURI) throws UWSException{
@@ -340,15 +351,15 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Builds a UWS with the given UWS URL interpreter.</p>
-	 * 
+	 *
 	 * <p><i><u>note:</u> since no logger is provided, a default one is set automatically (see {@link DefaultUWSLog}).</i></p>
-	 * 
+	 *
 	 * @param jobFactory	Object which lets creating the UWS jobs managed by this UWS and their thread/task.
 	 * @param fileManager	Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...).
 	 * @param urlInterpreter	The UWS URL interpreter to use in this UWS.
-	 * 
+	 *
 	 * @throws UWSException			If unable to create a request parser using the factory (see {@link UWSFactory#createRequestParser(UWSFileManager)}).
-	 * 
+	 *
 	 * @see #UWSService(UWSFactory, UWSFileManager, UWSLog, UWSUrl)
 	 */
 	public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSUrl urlInterpreter) throws UWSException{
@@ -357,12 +368,12 @@ public class UWSService implements UWS {
 
 	/**
 	 * Builds a UWS with the given UWS URL interpreter.
-	 * 
+	 *
 	 * @param jobFactory	Object which lets creating the UWS jobs managed by this UWS and their thread/task.
 	 * @param fileManager	Object which lets managing all files managed by this UWS (i.e. log, result, backup, error, ...).
 	 * @param logger		Object which lets printing any message (error, info, debug, warning).
 	 * @param urlInterpreter	The UWS URL interpreter to use in this UWS.
-	 * 
+	 *
 	 * @throws UWSException			If unable to create a request parser using the factory (see {@link UWSFactory#createRequestParser(UWSFileManager)}).
 	 */
 	public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger, final UWSUrl urlInterpreter) throws UWSException{
@@ -411,7 +422,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets the object used to write/format any error in a HttpServletResponse.
-	 * 
+	 *
 	 * @return The error writer/formatter.
 	 */
 	public final ServiceErrorWriter getErrorWriter(){
@@ -420,9 +431,9 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Sets the object used to write/format any error in a HttpServletResponse.</p>
-	 * 
+	 *
 	 * <p><i><u>Note:</u> Nothing is done if the given writer is NULL !</i></p>
-	 * 
+	 *
 	 * @param errorWriter The new error writer/formatter.
 	 */
 	public final void setErrorWriter(ServiceErrorWriter errorWriter){
@@ -440,7 +451,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Sets the name of this UWS.
-	 * 
+	 *
 	 * @param name	Its new name.
 	 */
 	public final void setName(String name){
@@ -454,7 +465,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Sets the description of this UWS.
-	 * 
+	 *
 	 * @param description	Its new description.
 	 */
 	public final void setDescription(String description){
@@ -463,9 +474,9 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets the base UWS URL.
-	 * 
+	 *
 	 * @return	The base UWS URL.
-	 * 
+	 *
 	 * @see UWSUrl#getBaseURI()
 	 */
 	public final String getBaseURI(){
@@ -479,7 +490,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Sets the UWS URL interpreter to use in this UWS.
-	 * 
+	 *
 	 * @param urlInterpreter	Its new UWS URL interpreter (may be <i>null</i>. In this case, it will be created from the next request ; see {@link #executeRequest(HttpServletRequest, HttpServletResponse)}).
 	 */
 	public final void setUrlInterpreter(UWSUrl urlInterpreter){
@@ -493,7 +504,7 @@ public class UWSService implements UWS {
 	/**
 	 * <p>Gets the object which lets extracting the user ID from a HTTP request.</p>
 	 * <p><i><u>note:</u>If the returned user identifier is NULL, no job should have an owner.</i></p>
-	 * 
+	 *
 	 * @return	The used UserIdentifier (MAY BE NULL).
 	 */
 	@Override
@@ -503,7 +514,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Sets the object which lets extracting the use ID from a received request.
-	 * 
+	 *
 	 * @param identifier	The UserIdentifier to use (may be <i>null</i>).
 	 */
 	public final void setUserIdentifier(UserIdentifier identifier){
@@ -530,7 +541,7 @@ public class UWSService implements UWS {
 	 * 	Sets its backup manager.
 	 * 	This manager will be called at each user action to save only its own jobs list by calling {@link UWSBackupManager#saveOwner(JobOwner)}.
 	 * </p>
-	 * 
+	 *
 	 * @param backupManager Its new backup manager.
 	 */
 	public final void setBackupManager(final UWSBackupManager backupManager){
@@ -542,12 +553,54 @@ public class UWSService implements UWS {
 		return requestParser;
 	}
 
+	/**
+	 * Get the currently used strategy for the blocking behavior of the
+	 * Job Summary action.
+	 *
+	 * <p>
+	 * 	This strategy lets decide how long a WAIT request must block a HTTP
+	 * 	request. With a such policy, the waiting time specified by the user may
+	 * 	be modified.
+	 * </p>
+	 *
+	 * @return	The WAIT strategy,
+	 *        	or NULL if the default one (i.e. wait the time specified by the
+	 *        	user) is used.
+	 *
+	 * @since 4.3
+	 */
+	public final BlockingPolicy getWaitPolicy(){
+		return waitPolicy;
+	}
+
+	/**
+	 * Set the strategy to use for the blocking behavior of the
+	 * Job Summary action.
+	 *
+	 * <p>
+	 * 	This strategy lets decide whether a WAIT request must block a HTTP
+	 * 	request and how long. With a such policy, the waiting time specified by
+	 * 	the user may be modified.
+	 * </p>
+	 *
+	 * @param waitPolicy	The WAIT strategy to use,
+	 *                  	or NULL if the default one (i.e. wait the time
+	 *                  	specified by the user ;
+	 *                  	if no time is specified the HTTP request may be
+	 *                  	blocked indefinitely) must be used.
+	 *
+	 * @since 4.3
+	 */
+	public final void setWaitPolicy(final BlockingPolicy waitPolicy){
+		this.waitPolicy = waitPolicy;
+	}
+
 	/* ******************** */
 	/* HOME PAGE MANAGEMENT */
 	/* ******************** */
 	/**
 	 * Gets the URL of the resource which must be used as home page of this UWS.
-	 * 
+	 *
 	 * @return	The URL of the home page.
 	 */
 	public final String getHomePage(){
@@ -556,7 +609,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Tells whether a redirection to the specified home page must be done or not.
-	 * 
+	 *
 	 * @return	<i>true</i> if a redirection to the specified resource must be done
 	 * 			or <i>false</i> to copy it.
 	 */
@@ -566,7 +619,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Sets the URL of the resource which must be used as home page of this UWS.
-	 * 
+	 *
 	 * @param homePageUrl	The URL of the home page (may be <i>null</i>).
 	 * @param redirect		<i>true</i> if a redirection to the specified resource must be done
 	 * 						or <i>false</i> to copy it.
@@ -579,7 +632,7 @@ public class UWSService implements UWS {
 	/**
 	 * <p>Sets the URI of the resource which must be used as home page of this UWS.</p>
 	 * <i>A redirection will always be done on the specified resource.</i>
-	 * 
+	 *
 	 * @param homePageURI	The URL of the home page.
 	 */
 	public final void setHomePage(String homePageURI){
@@ -590,7 +643,7 @@ public class UWSService implements UWS {
 	/**
 	 * Indicates whether the current home page is the default one (the UWS serialization)
 	 * or if it has been specified manually using {@link UWSService#setHomePage(URL, boolean)}.
-	 * 
+	 *
 	 * @return	<i>true</i> if it is the default home page, <i>false</i> otherwise.
 	 */
 	public final boolean isDefaultHomePage(){
@@ -608,16 +661,16 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Get the MIME type of the custom home page.</p>
-	 * 
+	 *
 	 * <p>By default, it is the same as the default home page: "text/html".</p>
-	 * 
+	 *
 	 * <p><i>Note:
 	 * 	This function has a sense only if the HOME PAGE resource of this UWS service
 	 * 	is still the default home page (i.e. {@link ShowHomePage}).
 	 * </i></p>
-	 * 
+	 *
 	 * @return	MIME type of the custom home page.
-	 * 
+	 *
 	 * @since 4.2
 	 */
 	public final String getHomePageMimeType(){
@@ -626,16 +679,16 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Set the MIME type of the custom home page.</p>
-	 * 
+	 *
 	 * <p>A NULL value will be considered as "text/html".</p>
-	 * 
+	 *
 	 * <p><i>Note:
 	 * 	This function has a sense only if the HOME PAGE resource of this UWS service
 	 * 	is still the default home page (i.e. {@link ShowHomePage}).
 	 * </i></p>
-	 * 
+	 *
 	 * @param mime	MIME type of the custom home page.
-	 * 
+	 *
 	 * @since 4.2
 	 */
 	public final void setHomePageMimeType(final String mime){
@@ -647,7 +700,7 @@ public class UWSService implements UWS {
 	/* ********************** */
 	/**
 	 * Gets the MIME type of the serializer to use by default.
-	 * 
+	 *
 	 * @return	The MIME type of the default serializer.
 	 */
 	public final String getDefaultSerializer(){
@@ -656,9 +709,9 @@ public class UWSService implements UWS {
 
 	/**
 	 * Sets the MIME of the serializer to use by default.
-	 * 
+	 *
 	 * @param mimeType		The MIME type (only one).
-	 * 
+	 *
 	 * @throws UWSException If there is no serializer with this MIME type available in this UWS.
 	 */
 	public final void setDefaultSerializer(String mimeType) throws UWSException{
@@ -672,7 +725,7 @@ public class UWSService implements UWS {
 	 * <p>Adds a serializer to this UWS</p>
 	 * <p><b><u>WARNING:</u> If there is already a serializer with the same MIME type (see {@link UWSSerializer#getMimeType()}) in this UWS ,
 	 * it should be replaced by the given one !</b></p>
-	 * 
+	 *
 	 * @param serializer	The serializer to add.
 	 * @return				<i>true</i> if the serializer has been successfully added, <i>false</i> otherwise.
 	 */
@@ -688,9 +741,9 @@ public class UWSService implements UWS {
 
 	/**
 	 * Tells whether this UWS has already a serializer with the given MIME type.
-	 * 
+	 *
 	 * @param mimeType	A MIME type (only one).
-	 * 
+	 *
 	 * @return			<i>true</i> if a serializer exists with the given MIME type, <i>false</i> otherwise.
 	 */
 	public final boolean hasSerializerFor(String mimeType){
@@ -699,7 +752,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets the total number of serializers available in this UWS.
-	 * 
+	 *
 	 * @return	The number of its serializers.
 	 */
 	public final int getNbSerializers(){
@@ -708,7 +761,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets an iterator of the list of all serializers available in this UWS.
-	 * 
+	 *
 	 * @return	An iterator on its serializers.
 	 */
 	public final Iterator<UWSSerializer> getSerializers(){
@@ -741,7 +794,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets the serializer choosen during the last call of {@link #getSerializer(String)}.
-	 * 
+	 *
 	 * @return	The last used serializer.
 	 */
 	public final UWSSerializer getChoosenSerializer(){
@@ -750,7 +803,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Removes the serializer whose the MIME type is the same as the given one.
-	 * 
+	 *
 	 * @param mimeType	MIME type of the serializer to remove.
 	 * @return			The removed serializer
 	 * 					or <i>null</i> if no corresponding serializer has been found.
@@ -761,7 +814,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets the URL of the XSLT style-sheet that the XML serializer of this UWS is using.
-	 * 
+	 *
 	 * @return	The used XSLT URL.
 	 */
 	public final String getXsltURL(){
@@ -773,9 +826,9 @@ public class UWSService implements UWS {
 
 	/**
 	 * Sets the URL of the XSLT style-sheet that the XML serializer of this UWS must use.
-	 * 
+	 *
 	 * @param xsltPath	The new XSLT URL.
-	 * 
+	 *
 	 * @return			<i>true</i> if the given path/url has been successfully set, <i>false</i> otherwise.
 	 */
 	public final boolean setXsltURL(String xsltPath){
@@ -792,7 +845,7 @@ public class UWSService implements UWS {
 	/* ********************* */
 	/**
 	 * An iterator on the jobs lists list.
-	 * 
+	 *
 	 * @see java.lang.Iterable#iterator()
 	 */
 	@Override
@@ -812,13 +865,13 @@ public class UWSService implements UWS {
 
 	/**
 	 * Adds a jobs list to this UWS.
-	 * 
+	 *
 	 * @param jl	The jobs list to add.
-	 * 
+	 *
 	 * @return		<i>true</i> if the jobs list has been successfully added,
 	 * 				<i>false</i> if the given jobs list is <i>null</i> or if a jobs list with this name already exists
 	 * 				or if a UWS is already associated with another UWS.
-	 * 
+	 *
 	 * @see JobList#setUWS(UWS)
 	 * @see UWS#addJobList(JobList)
 	 */
@@ -847,11 +900,11 @@ public class UWSService implements UWS {
 
 	/**
 	 * Destroys the given jobs list.
-	 * 
+	 *
 	 * @param jl	The jobs list to destroy.
-	 * 
+	 *
 	 * @return	<i>true</i> if the given jobs list has been destroyed, <i>false</i> otherwise.
-	 * 
+	 *
 	 * @see JobList#clear()
 	 * @see JobList#setUWS(UWS)
 	 */
@@ -873,7 +926,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Destroys all managed jobs lists.
-	 * 
+	 *
 	 * @see #destroyJobList(String)
 	 */
 	public final void destroyAllJobLists(){
@@ -887,12 +940,12 @@ public class UWSService implements UWS {
 	/* ********************** */
 	/**
 	 * <p>Lets adding the given action to this UWS.</p>
-	 * 
+	 *
 	 * <p><b><u>WARNING:</u> The action will be added at the end of the actions list of this UWS. That means, it will be evaluated (call of
 	 * the method {@link UWSAction#match(UWSUrl, JobOwner, HttpServletRequest)}) lastly !</b></p>
-	 * 
+	 *
 	 * @param action	The UWS action to add.
-	 * 
+	 *
 	 * @return			<i>true</i> if the given action has been successfully added, <i>false</i> otherwise.
 	 */
 	public final boolean addUWSAction(UWSAction action){
@@ -904,12 +957,12 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Lets inserting the given action at the given position in the actions list of this UWS.</p>
-	 * 
+	 *
 	 * @param indAction							The index where the given action must be inserted.
 	 * @param action							The action to add.
-	 * 
+	 *
 	 * @return									<i>true</i> if the given action has been successfully added, <i>false</i> otherwise.
-	 * 
+	 *
 	 * @throws ArrayIndexOutOfBoundsException	If the given index is incorrect (index < 0 || index >= uwsActions.size()).
 	 */
 	public final boolean addUWSAction(int indAction, UWSAction action) throws ArrayIndexOutOfBoundsException{
@@ -922,12 +975,12 @@ public class UWSService implements UWS {
 
 	/**
 	 * Replaces the specified action by the given action.
-	 * 
+	 *
 	 * @param indAction							Index of the action to replace.
 	 * @param action							The replacer.
-	 * 
+	 *
 	 * @return									<i>true</i> if the replacement has been a success, <i>false</i> otherwise.
-	 * 
+	 *
 	 * @throws ArrayIndexOutOfBoundsException	If the index is incorrect (index < 0 || index >= uwsActions.size()).
 	 */
 	public final boolean setUWSAction(int indAction, UWSAction action) throws ArrayIndexOutOfBoundsException{
@@ -940,9 +993,9 @@ public class UWSService implements UWS {
 
 	/**
 	 * Replaces the action which has the same name that the given action.
-	 * 
+	 *
 	 * @param action	The replacer.
-	 * 
+	 *
 	 * @return			The replaced action
 	 * 					or <i>null</i> if the given action is <i>null</i>
 	 * 									or if there is no action with the same name (in this case, the given action is not added).
@@ -961,7 +1014,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets the number of actions this UWS has.
-	 * 
+	 *
 	 * @return	The number of its actions.
 	 */
 	public final int getNbUWSActions(){
@@ -970,9 +1023,9 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets the action of this UWS which has the same name as the given one.
-	 * 
+	 *
 	 * @param actionName	The name of the searched action.
-	 * 
+	 *
 	 * @return				The corresponding action
 	 * 						or <i>null</i> if there is no corresponding action.
 	 */
@@ -986,7 +1039,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets all actions of this UWS.
-	 * 
+	 *
 	 * @return	An iterator on its actions.
 	 */
 	public final Iterator<UWSAction> getUWSActions(){
@@ -995,7 +1048,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Gets the UWS action executed during the last call of {@link #executeRequest(HttpServletRequest, HttpServletResponse)}.
-	 * 
+	 *
 	 * @return	The last used UWS action.
 	 */
 	public final UWSAction getExecutedAction(){
@@ -1004,11 +1057,11 @@ public class UWSService implements UWS {
 
 	/**
 	 * Removes the specified action from this UWS.
-	 * 
+	 *
 	 * @param indAction							The index of the UWS action to remove.
-	 * 
+	 *
 	 * @return									The removed action.
-	 * 
+	 *
 	 * @throws ArrayIndexOutOfBoundsException	If the given index is incorrect (index < 0 || index >= uwsActions.size()).
 	 */
 	public final UWSAction removeUWSAction(int indAction) throws ArrayIndexOutOfBoundsException{
@@ -1017,7 +1070,7 @@ public class UWSService implements UWS {
 
 	/**
 	 * Removes the action of this UWS which has the same name as the given one.
-	 * 
+	 *
 	 * @param actionName	The name of the UWS to remove.
 	 * @return				The removed action
 	 * 						or <i>null</i> if there is no corresponding action.
@@ -1036,13 +1089,13 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Generate a unique ID for the given request.</p>
-	 * 
+	 *
 	 * <p>By default, a timestamp is returned.</p>
-	 * 
+	 *
 	 * @param request	Request whose an ID is asked.
-	 * 
+	 *
 	 * @return	The ID of the given request.
-	 * 
+	 *
 	 * @since 4.1
 	 */
 	protected synchronized String generateRequestID(final HttpServletRequest request){
@@ -1057,7 +1110,7 @@ public class UWSService implements UWS {
 	/**
 	 * <p>Executes the given request according to the <a href="http://www.ivoa.net/Documents/UWS/20100210/">IVOA Proposed Recommendation of 2010-02-10</a>.
 	 * The result is returned in the given response.</p>
-	 * 
+	 *
 	 * <p>Here is the followed algorithm:</p>
 	 * <ol>
 	 * 	<li>Load the request in the UWS URL interpreter (see {@link UWSUrl#load(HttpServletRequest)})</li>
@@ -1065,15 +1118,15 @@ public class UWSService implements UWS {
 	 * 	<li>Iterate - in order - on all available actions and apply the first which matches.
 	 * 		(see {@link UWSAction#match(UWSUrl, JobOwner, HttpServletRequest)} and {@link UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse)})</li>
 	 * </ol>
-	 * 
+	 *
 	 * @param request		The UWS request.
 	 * @param response		The response of this request which will be edited by the found UWS actions.
-	 * 
+	 *
 	 * @return				<i>true</i> if the request has been executed successfully, <i>false</i> otherwise.
-	 * 
+	 *
 	 * @throws UWSException	If no action matches or if any error has occurred while applying the found action.
 	 * @throws IOException	If it is impossible to write in the given {@link HttpServletResponse}.
-	 * 
+	 *
 	 * @see UWSUrl#UWSUrl(HttpServletRequest)
 	 * @see UWSUrl#load(HttpServletRequest)
 	 * @see UserIdentifier#extractUserId(UWSUrl, HttpServletRequest)
@@ -1200,13 +1253,13 @@ public class UWSService implements UWS {
 
 	/**
 	 * <p>Sends a redirection (with the HTTP status code 303) to the given URL/URI into the given response.</p>
-	 * 
+	 *
 	 * @param url		The redirection URL/URI.
 	 * @param request	The {@link HttpServletRequest} which may be used to make a redirection.
 	 * @param user		The user which executes the given request.
 	 * @param uwsAction	The UWS action corresponding to the given request.
 	 * @param response	The {@link HttpServletResponse} which must contain all information to make a redirection.
-	 * 
+	 *
 	 * @throws IOException	If there is an error during the redirection.
 	 * @throws UWSException	If there is any other error.
 	 */
@@ -1224,17 +1277,17 @@ public class UWSService implements UWS {
 	 * 	If the error code is {@link UWSException#SEE_OTHER} this method calls {@link #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)}.
 	 * 	Otherwise the function {@link HttpServletResponse#sendError(int, String)} is called.
 	 * </p>
-	 * 
+	 *
 	 * @param error			The error to send/display.
 	 * @param request		The request which has caused the given error <i>(not used by default)</i>.
 	 * @param reqID			ID of the request.
 	 * @param user			The user which executes the given request.
 	 * @param uwsAction	The UWS action corresponding to the given request.
 	 * @param response		The response in which the error must be published.
-	 * 
+	 *
 	 * @throws IOException	If there is an error when calling {@link #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)} or {@link HttpServletResponse#sendError(int, String)}.
 	 * @throws UWSException	If there is an error when calling {@link #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)}.
-	 * 
+	 *
 	 * @see #redirect(String, HttpServletRequest, JobOwner, String, HttpServletResponse)
 	 * @see #sendError(Throwable, HttpServletRequest, String, JobOwner, String, HttpServletResponse)
 	 */
@@ -1255,17 +1308,17 @@ public class UWSService implements UWS {
 	 * 	{@link HttpServletResponse#sendError(int, String)} is called with the HTTP status code is {@link UWSException#INTERNAL_SERVER_ERROR}
 	 * 	and the message of the given exception.
 	 * </p>
-	 * 
-	 * 
+	 *
+	 *
 	 * @param error			The error to send/display.
 	 * @param request		The request which has caused the given error <i>(not used by default)</i>.
 	 * @param reqID			ID of the request.
 	 * @param user			The user which executes the given request.
 	 * @param uwsAction		The UWS action corresponding to the given request.
 	 * @param response		The response in which the error must be published.
-	 * 
+	 *
 	 * @throws IOException	If there is an error when calling {@link HttpServletResponse#sendError(int, String)}.
-	 * 
+	 *
 	 * @see ServiceErrorWriter#writeError(Throwable, HttpServletResponse, HttpServletRequest, String, JobOwner, String)
 	 */
 	public final void sendError(Throwable error, HttpServletRequest request, String reqID, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException{
diff --git a/src/uws/service/UWSServlet.java b/src/uws/service/UWSServlet.java
index e2015fb..ebbde87 100644
--- a/src/uws/service/UWSServlet.java
+++ b/src/uws/service/UWSServlet.java
@@ -59,6 +59,7 @@ import uws.job.serializer.UWSSerializer;
 import uws.job.serializer.XMLSerializer;
 import uws.job.serializer.filter.JobListRefiner;
 import uws.job.user.JobOwner;
+import uws.service.actions.JobSummary;
 import uws.service.actions.UWSAction;
 import uws.service.backup.UWSBackupManager;
 import uws.service.error.DefaultUWSErrorWriter;
@@ -71,6 +72,7 @@ import uws.service.log.UWSLog.LogLevel;
 import uws.service.request.RequestParser;
 import uws.service.request.UWSRequestParser;
 import uws.service.request.UploadFile;
+import uws.service.wait.BlockingPolicy;
 
 /**
  * <p>
@@ -151,7 +153,7 @@ import uws.service.request.UploadFile;
  * </p>
  *
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 4.3 (10/2017)
+ * @version 4.3 (11/2017)
  */
 public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory {
 	private static final long serialVersionUID = 1L;
@@ -199,6 +201,17 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory
 	/** Lets writing/formatting any exception/throwable in a HttpServletResponse. */
 	protected ServiceErrorWriter errorWriter;
 
+	/**
+	 * Strategy to use for the blocking/wait process concerning the
+	 * {@link #doJobSummary(UWSUrl, HttpServletRequest, HttpServletResponse, JobOwner)}
+	 * action.
+	 * <p>
+	 * 	If NULL, the standard strategy will be used: wait exactly the time asked
+	 * 	by the user (or indefinitely if none is specified).
+	 * </p>
+	 * @since 4.3 */
+	protected BlockingPolicy waitPolicy = null;
+
 	@Override
 	public final void init(ServletConfig config) throws ServletException{
 		super.init(config);
@@ -503,6 +516,52 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory
 		}
 	}
 
+	/* ***************** */
+	/* BLOCKING BEHAVIOR */
+	/* ***************** */
+
+	/**
+	 * Get the currently used strategy for the blocking behavior of the
+	 * Job Summary action.
+	 *
+	 * <p>
+	 * 	This strategy lets decide how long a WAIT request must block a HTTP
+	 * 	request. With a such policy, the waiting time specified by the user may
+	 * 	be modified.
+	 * </p>
+	 *
+	 * @return	The WAIT strategy,
+	 *        	or NULL if the default one (i.e. wait the time specified by the
+	 *        	user) is used.
+	 *
+	 * @since 4.3
+	 */
+	public final BlockingPolicy getWaitPolicy(){
+		return waitPolicy;
+	}
+
+	/**
+	 * Set the strategy to use for the blocking behavior of the
+	 * Job Summary action.
+	 *
+	 * <p>
+	 * 	This strategy lets decide whether a WAIT request must block a HTTP
+	 * 	request and how long. With a such policy, the waiting time specified by
+	 * 	the user may be modified.
+	 * </p>
+	 *
+	 * @param waitPolicy	The WAIT strategy to use,
+	 *                  	or NULL if the default one (i.e. wait the time
+	 *                  	specified by the user ;
+	 *                  	if no time is specified the HTTP request may be
+	 *                  	blocked indefinitely) must be used.
+	 *
+	 * @since 4.3
+	 */
+	public final void setWaitPolicy(final BlockingPolicy waitPolicy){
+		this.waitPolicy = waitPolicy;
+	}
+
 	/* *********** */
 	/* UWS ACTIONS */
 	/* *********** */
@@ -604,6 +663,9 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory
 		// Get the job:
 		UWSJob job = getJob(requestUrl);
 
+		// Block if necessary:
+		JobSummary.block(waitPolicy, req, job, user);
+
 		// Write the job summary:
 		UWSSerializer serializer = getSerializer(req.getHeader("Accept"));
 		resp.setContentType(serializer.getMimeType());
diff --git a/src/uws/service/actions/JobSummary.java b/src/uws/service/actions/JobSummary.java
index e35e1d8..89acba1 100644
--- a/src/uws/service/actions/JobSummary.java
+++ b/src/uws/service/actions/JobSummary.java
@@ -2,25 +2,26 @@ package uws.service.actions;
 
 /*
  * This file is part of UWSLibrary.
- * 
+ *
  * UWSLibrary is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published by
  * the Free Software Foundation, either version 3 of the License, or
  * (at your option) any later version.
- * 
+ *
  * UWSLibrary is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU Lesser General Public License for more details.
- * 
+ *
  * 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-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ *
+ * Copyright 2012-2017 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
  *                       Astronomisches Rechen Institut (ARI)
  */
 
 import java.io.IOException;
+import java.util.Enumeration;
 
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletRequest;
@@ -28,27 +29,40 @@ import javax.servlet.http.HttpServletResponse;
 
 import uws.UWSException;
 import uws.UWSToolBox;
+import uws.job.ExecutionPhase;
 import uws.job.UWSJob;
 import uws.job.serializer.UWSSerializer;
 import uws.job.user.JobOwner;
 import uws.service.UWSService;
 import uws.service.UWSUrl;
 import uws.service.log.UWSLog.LogLevel;
+import uws.service.wait.BlockingPolicy;
+import uws.service.wait.WaitObserver;
 
 /**
- * <p>The "Get Job" action of a UWS.</p>
- * 
- * <p><i><u>Note:</u> The corresponding name is {@link UWSAction#JOB_SUMMARY}.</i></p>
- * 
- * <p>This action returns the summary of the job specified in the given UWS URL.
- * This summary is serialized by the {@link UWSSerializer} choosed in function of the HTTP Accept header.</p>
- * 
+ * The "Get Job" action of a UWS.
+ *
+ * <p><i>Note:
+ * 	The corresponding name is {@link UWSAction#JOB_SUMMARY}.
+ * </i></p>
+ *
+ * <p>
+ * 	This action returns the summary of the job specified in the given UWS URL.
+ * 	This summary is serialized by the {@link UWSSerializer} chosen in function
+ * 	of the HTTP Accept header.
+ * </p>
+ *
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 4.1 (04/2015)
+ * @version 4.3 (11/2017)
  */
 public class JobSummary extends UWSAction {
 	private static final long serialVersionUID = 1L;
 
+	/** Name of the parameter which allows the blocking behavior
+	 * (for a specified or unlimited duration) of a {@link JobSummary} request.
+	 * @since 4.3 */
+	public final static String WAIT_PARAMETER = "WAIT";
+
 	public JobSummary(UWSService u){
 		super(u);
 	}
@@ -70,12 +84,15 @@ public class JobSummary extends UWSAction {
 	/**
 	 * Checks whether:
 	 * <ul>
-	 * 	<li>a job list name is specified in the given UWS URL <i>(<u>note:</u> the existence of the jobs list is not checked)</i>,</li>
-	 * 	<li>a job ID is given in the UWS URL <i>(<u>note:</u> the existence of the job is not checked)</i>,</li>
+	 * 	<li>a job list name is specified in the given UWS URL
+	 * 		<i>(<u>note:</u> the existence of the jobs list is not checked)</i>,
+	 * 	</li>
+	 * 	<li>a job ID is given in the UWS URL
+	 * 		<i>(<u>note:</u> the existence of the job is not checked)</i>,</li>
 	 * 	<li>there is no job attribute,</li>
 	 * 	<li>the HTTP method is HTTP-GET.</li>
 	 * </ul>
-	 * 
+	 *
 	 * @see uws.service.actions.UWSAction#match(UWSUrl, JobOwner, HttpServletRequest)
 	 */
 	@Override
@@ -85,12 +102,13 @@ public class JobSummary extends UWSAction {
 
 	/**
 	 * Gets the specified job <i>(and throw an error if not found)</i>,
-	 * chooses the serializer and write the serialization of the job in the given response.
-	 * 
+	 * chooses the serializer and write the serialization of the job in the
+	 * given response.
+	 *
 	 * @see #getJob(UWSUrl)
 	 * @see UWSService#getSerializer(String)
 	 * @see UWSJob#serialize(ServletOutputStream, UWSSerializer)
-	 * 
+	 *
 	 * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse)
 	 */
 	@Override
@@ -98,6 +116,9 @@ public class JobSummary extends UWSAction {
 		// Get the job:
 		UWSJob job = getJob(urlInterpreter);
 
+		// Block if necessary:
+		JobSummary.block(uws.getWaitPolicy(), request, job, user);
+
 		// Write the job summary:
 		UWSSerializer serializer = uws.getSerializer(request.getHeader("Accept"));
 		response.setContentType(serializer.getMimeType());
@@ -115,4 +136,172 @@ public class JobSummary extends UWSAction {
 		return true;
 	}
 
+	/**
+	 * Block the current thread until the specified duration (in seconds) is
+	 * elapsed or if the execution phase of the target job changes.
+	 *
+	 * <p>
+	 * 	A blocking is performed only if the given job is in an active phase
+	 * 	(i.e. PENDING, QUEUED or EXECUTING).
+	 * </p>
+	 *
+	 * <p>This function expects the 2 following HTTP-GET parameters:</p>
+	 * <ul>
+	 * 	<li><b>WAIT</b>: <i>[MANDATORY]</i> with a value (in seconds). The value
+	 * 		             must be a positive and not null integer expressing a
+	 * 		             duration in seconds or -1 (or any other negative value)
+	 * 		             for an infinite time. If a not legal value or no value
+	 * 		             is provided, the parameter will be merely ignored.
+	 * 		             <br/>
+	 * 	                 This parameter raises a flag meaning a blocking is
+	 * 		             required (except if 0 is provided) and eventually a
+	 * 		             time (in seconds) to wait before stop blocking.
+	 * 		             <br/>
+	 * 	                 If several values are provided, only the one meaning
+	 * 		             the smallest blocking waiting time will be kept.
+	 * 		             Particularly if both a negative and a positive or null
+	 * 		             value are given, only the positive or null value will
+	 * 		             be kept.</li>
+	 *
+	 * 	<li><b>PHASE</b>: <i>[OPTIONAL]</i> A legal execution phase must be
+	 * 		              provided, otherwise this parameter will be ignored.
+	 * 		              <br/>
+	 * 	                  This parameter indicates the phase in which the job
+	 * 		              must be at the time the blocking is required. If the
+	 * 		              current job phase is different from the specified one,
+	 * 		              no blocking will be performed. Note that the allowed
+	 * 		              phases are PENDING, QUEUED and EXECUTING, because only
+	 * 		              a job in one of these phases can be blocked.
+	 * 		              <br/>
+	 * 	                  If several values are provided, only the last
+	 * 		              occurrence is kept.</li>
+	 * </ul>
+	 *
+	 * <p><i>Note:
+	 * 	A waiting time of 0 will be interpreted as "no blocking".
+	 * </i></p>
+	 *
+	 * <p><i>Note:
+	 * 	This function will have no effect if the given thread, the given HTTP
+	 * 	request or the given job is NULL.
+	 * </i></p>
+	 *
+	 * @param policy		Strategy to adopt for the blocking behavior.
+	 *              		<i>If NULL, the standard blocking behavior will be
+	 *              		performed: block the duration (eventually unlimited)
+	 *              		specified by the user.</i>
+	 * @param req			The HTTP request which asked for the blocking.
+	 *           			<b>MUST NOT be NULL, otherwise no blocking will be
+	 *           			performed.</b>
+	 * @param job			The job associate with the HTTP request.
+	 *           			<b>MUST NOT be NULL, otherwise no blocking will be
+	 *           			performed.</b>
+	 * @param user			The user who asked for the blocking behavior.
+	 *            			<i>NULL if no user is logged in.</i>
+	 *
+	 * @since 4.3
+	 */
+	public static void block(final BlockingPolicy policy, final HttpServletRequest req, final UWSJob job, final JobOwner user){
+		if (req == null || job == null)
+			return;
+
+		/* No blocking if the job is not in an "active" phase: */
+		if (job.getPhase() != ExecutionPhase.PENDING && job.getPhase() != ExecutionPhase.QUEUED && job.getPhase() != ExecutionPhase.EXECUTING)
+			return;
+
+		/* Extract the parameters WAIT (only the smallest waiting time is taken
+		 * into account) and PHASE (only the last legal occurrence is taken into
+		 * account): */
+		ExecutionPhase phase = null;
+		boolean waitGiven = false;
+		long waitingTime = 0;
+		String param;
+		String[] values;
+		Enumeration<String> parameters = req.getParameterNames();
+		while(parameters.hasMoreElements()){
+			param = parameters.nextElement();
+			values = req.getParameterValues(param);
+			// CASE: WAIT parameter
+			if (param.toUpperCase().equals("WAIT")){
+				/* note: a value MUST be given for a WAIT parameter ; if it is
+				 *       missing the parameter is ignored */
+				if (values != null){
+					for(int i = 0; i < values.length; i++){
+						try{
+							if (values[i] != null && values[i].trim().length() > 0){
+								long tmp = Long.parseLong(values[i]);
+								if (tmp < 0 && !waitGiven)
+									waitingTime = tmp;
+								else if (tmp >= 0)
+									waitingTime = (waitGiven && waitingTime >= 0) ? Math.min(waitingTime, tmp) : tmp;
+								waitGiven = true;
+							}
+						}catch(NumberFormatException nfe){}
+					}
+				}
+			}
+			// CASE: PHASE parameter
+			else if (param.toUpperCase().equals("PHASE") && values != null){
+				for(int i = values.length - 1; phase == null && i >= 0; i--){
+					try{
+						if (values[i].trim().length() > 0)
+							phase = ExecutionPhase.valueOf(values[i].toUpperCase());
+					}catch(IllegalArgumentException iae){}
+				}
+			}
+		}
+
+		/* The HTTP-GET request should block until either the specified time
+		 * (or the timeout) is reached or if the job phase changed: */
+		if (waitingTime != 0 && (phase == null || job.getPhase() == phase)){
+			Thread threadToBlock = Thread.currentThread();
+			WaitObserver observer = null;
+
+			/* Eventually limit the waiting time in function of the chosen
+			 * policy: */
+			if (policy != null)
+				waitingTime = policy.block(threadToBlock, waitingTime, job, user, req);
+
+			/* Blocking ONLY IF the duration is NOT NULL (i.e. wait during 0
+			 * seconds): */
+			if (waitingTime != 0){
+				try{
+					/* Watch the job in order to detect an execution phase
+					 * modification: */
+					observer = new WaitObserver(threadToBlock);
+					job.addObserver(observer);
+
+					/* If the job is still processing, then wait the specified
+					 * time: */
+					if (job.getPhase() == ExecutionPhase.PENDING || job.getPhase() == ExecutionPhase.QUEUED || job.getPhase() == ExecutionPhase.EXECUTING){
+						synchronized(threadToBlock){
+							// Limited duration:
+							if (waitingTime > 0)
+								threadToBlock.wait(waitingTime * 1000);
+							/* "Unlimited" duration (the wait will stop only if
+							 * the job phase changes): */
+							else
+								threadToBlock.wait();
+						}
+					}
+
+				}catch(InterruptedException ie){
+					/* If the WAIT has been interrupted, the blocking
+					 * is stopped and nothing special should happen. */
+				}
+				/* Clear all retained resources. */
+				finally{
+					// Do not observe any more the job:
+					if (observer != null)
+						job.removeObserver(observer);
+
+					/* Notify the BlockingPolicy that this Thread is no longer
+					 * blocked: */
+					if (policy != null)
+						policy.unblocked(threadToBlock, job, user, req);
+				}
+			}
+		}
+	}
+
 }
diff --git a/src/uws/service/wait/BlockingPolicy.java b/src/uws/service/wait/BlockingPolicy.java
new file mode 100644
index 0000000..0389f77
--- /dev/null
+++ b/src/uws/service/wait/BlockingPolicy.java
@@ -0,0 +1,114 @@
+package uws.service.wait;
+
+/*
+ * This file is part of UWSLibrary.
+ *
+ * UWSLibrary is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * UWSLibrary is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * 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 2017 - Astronomisches Rechen Institut (ARI)
+ */
+
+import javax.servlet.http.HttpServletRequest;
+
+import uws.job.UWSJob;
+import uws.job.user.JobOwner;
+
+/**
+ * Implementations of this interface define the policy to apply when a blocking
+ * of a request is asked by a UWS client.
+ *
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 4.3 (11/2017)
+ * @since 4.3
+ */
+public interface BlockingPolicy {
+
+	/**
+	 * Notify this {@link BlockingPolicy} that the given thread is going to
+	 * be blocked for the specified duration. This function then decides how
+	 * long the given thread must really wait before resuming.
+	 *
+	 * <p>
+	 * 	The parameter "userDuration" and the returned value are durations
+	 * 	expressed in seconds. Both follow the same rules:
+	 * </p>
+	 * <ul>
+	 * 	<li><u>If &lt; 0</u>, the request will wait theoretically
+	 * 		                  indefinitely.</li>
+	 * 	<li><u>If 0</u>, the request will return immediately ; no wait.</li>
+	 * 	<li><u>If &gt; 0</u>, the request will wait for the specified amount of
+	 * 		                  seconds.</li>
+	 * </ul>
+	 *
+	 * <p>
+	 * 	Since a timeout or another special behavior may be chosen by this
+	 * 	{@link BlockingPolicy}, the returned value may be different from the
+	 * 	user's asked duration. The value that should be taken into account is
+	 * 	obviously the returned one.
+	 * </p>
+	 *
+	 * <p><i><b>IMPORTANT:</b>
+	 * 	This function may <b>UN</b>block an older request/thread, in function of
+	 * 	the strategy chosen/implemented by this {@link BlockingPolicy}.
+	 * </i></p>
+	 *
+	 * @param thread		Thread that is going to be blocked.
+	 *                      <i>MUST NOT be NULL. If NULL this function will
+	 *                      either do nothing and return 0 (no blocking)
+	 *                      or throw a {@link NullPointerException}.</i>
+	 * @param userDuration	Waiting duration (in seconds) asked by the user.
+	 *                    	<i>&lt; 0 means indefinite, 0 means no wait and
+	 *                    	&gt; 0 means waiting for the specified amount of
+	 *                    	seconds.</i>
+	 * @param job			The job associated with the thread.
+	 *           			<i>Should not be NULL.</i>
+	 * @param user			The user who asked for the blocking behavior.
+	 *            			<i>If NULL, the request will be concerned as
+	 *            			anonymous and a decision to identify the user
+	 *            			(e.g. use the IP address) may be chosen by the
+	 *            			{@link BlockingPolicy} implementation if
+	 *            			required.</i>
+	 * @param request		The request which is going to be blocked.
+	 *               		<i>Should not be NULL.</i>
+	 *
+	 * @return	The real duration (in seconds) that the UWS service must wait
+	 *        	before returning a response to the given HTTP request.
+	 *        	<i>&lt; 0 means indefinite, 0 means no wait and &gt; 0 means
+	 *        	waiting for the specified amount of seconds.</i>
+	 *
+	 * @throws NullPointerException	If the given thread is NULL.
+	 */
+	public long block(final Thread thread, final long userDuration, final UWSJob job, final JobOwner user, final HttpServletRequest request) throws NullPointerException;
+
+	/**
+	 * Notify this {@link BlockingPolicy} that the given thread is not blocked
+	 * anymore.
+	 *
+	 * @param unblockedThread	Thread that is not blocked any more.
+	 *                       	<b>MUST be NOT NULL.</b>
+	 * @param job				The job associated with the unblocked Thread.
+	 *           				<i>Should not be NULL.</i>
+	 * @param user				The user who originally asked for the blocking
+	 *            				behavior.
+	 *            				<i>If NULL, the request will be concerned as
+	 *            				anonymous and a decision to identify the user
+	 *            				(e.g. use the IP address) may be chosen by the
+	 *            				{@link BlockingPolicy} implementation if
+	 *            				required.</i>
+	 * @param request			The request which has been unblocked.
+	 *               			<i>Should not be NULL.</i>
+	 */
+	public void unblocked(final Thread unblockedThread, final UWSJob job, final JobOwner user, final HttpServletRequest request);
+
+}
\ No newline at end of file
diff --git a/src/uws/service/wait/LimitedBlockingPolicy.java b/src/uws/service/wait/LimitedBlockingPolicy.java
new file mode 100644
index 0000000..82ff709
--- /dev/null
+++ b/src/uws/service/wait/LimitedBlockingPolicy.java
@@ -0,0 +1,89 @@
+package uws.service.wait;
+
+/*
+ * This file is part of UWSLibrary.
+ *
+ * UWSLibrary is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * UWSLibrary is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * 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 2017 - Astronomisches Rechen Institut (ARI)
+ */
+
+import javax.servlet.http.HttpServletRequest;
+
+import uws.job.UWSJob;
+import uws.job.user.JobOwner;
+
+/**
+ * In this {@link BlockingPolicy}, the blocking is limited by a maximum
+ * waiting time. Thus, unlimited blocking is prevented.
+ *
+ * <p>
+ * 	More concretely, if no timeout is specified by the user, {@link #timeout} is
+ * 	returned.
+ * </p>
+ *
+ * <p>
+ * 	If at creation of this policy no timeout is specified, a default one is set:
+ * 	{@link #DEFAULT_TIMEOUT} (= {@value #DEFAULT_TIMEOUT} seconds).
+ * </p>
+ *
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 4.3 (11/2017)
+ * @since 4.3
+ */
+public class LimitedBlockingPolicy implements BlockingPolicy {
+
+	/** Default timeout (in seconds) set by this policy at creation if none is
+	 * specified. */
+	public final static long DEFAULT_TIMEOUT = 60;
+
+	/** Maximum blocking duration (in seconds).
+	 * <i>This attribute is set by default to {@link #DEFAULT_TIMEOUT}. */
+	protected long timeout = DEFAULT_TIMEOUT;
+
+	/**
+	 * Build a blocking policy with the default timeout
+	 * (= {@value #DEFAULT_TIMEOUT} seconds).
+	 */
+	public LimitedBlockingPolicy(){}
+
+	/**
+	 * Build a {@link BlockingPolicy} which will limit blocking duration to the
+	 * given value.
+	 *
+	 * <p><i>IMPORTANT:
+	 * 	If {@link #timeout} is &lt; 0, the default timeout
+	 * 	(i.e. {@value #DEFAULT_TIMEOUT}) will be set automatically by this
+	 * 	constructor.
+	 * </i></p>
+	 *
+	 * @param timeout	Maximum blocking duration (in seconds).
+	 */
+	public LimitedBlockingPolicy(final long timeout){
+		this.timeout = (timeout < 0) ? DEFAULT_TIMEOUT : timeout;
+	}
+
+	@Override
+	public long block(final Thread thread, final long userDuration, final UWSJob job, final JobOwner user, final HttpServletRequest request){
+		// Nothing should happen if no thread and/or no job is provided:
+		if (job == null || thread == null)
+			return 0;
+
+		return (userDuration < 0 || userDuration > timeout) ? timeout : userDuration;
+	}
+
+	@Override
+	public void unblocked(final Thread unblockedThread, final UWSJob job, final JobOwner user, final HttpServletRequest request){}
+
+}
\ No newline at end of file
diff --git a/src/uws/service/wait/UserLimitedBlockingPolicy.java b/src/uws/service/wait/UserLimitedBlockingPolicy.java
new file mode 100644
index 0000000..5b875ba
--- /dev/null
+++ b/src/uws/service/wait/UserLimitedBlockingPolicy.java
@@ -0,0 +1,311 @@
+package uws.service.wait;
+
+/*
+ * This file is part of UWSLibrary.
+ *
+ * UWSLibrary is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * UWSLibrary is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * 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 2017 - Astronomisches Rechen Institut (ARI)
+ */
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+import javax.servlet.http.HttpServletRequest;
+
+import uws.job.UWSJob;
+import uws.job.user.JobOwner;
+
+/**
+ * This {@link BlockingPolicy} extends the {@link LimitedBlockingPolicy}.
+ * It proposes to limit the blocking duration, but it also limits the
+ * number of blocked threads for a given job and user.
+ *
+ * <h3>Blocked per Job AND User</h3>
+ *
+ * <p>
+ * 	The limit on the number of threads is valid ONLY for a given job AND
+ * 	a given user. For example, let's assume there is a limit
+ * 	of N blocking requests per job and user. The user U1 can start maximum N
+ * 	blocking requests to access the job J1 but not more. During this
+ * 	time he can also start up to N blocking access requests to any other job.
+ * 	And since this limit is valid only per user, another user U2 can also
+ * 	start up to N blocking requests on the job J1 without being affected by the
+ * 	fact that the limit is reached by the user U1 on this same job.
+ * </p>
+ *
+ * <p><i>Note:
+ * 	If no user is identified, the IP address will be used instead.
+ * </i></p>
+ *
+ * <h3>What happens when the limit is reached?</h3>
+ *
+ * <p>In a such case, 2 strategies are proposed:</p>
+ * <ul>
+ * 	<li>unblock the oldest blocked thread and accept the new blocking</li>
+ * 	<li>do not block for the new asked blocking (then
+ * 		{@link #block(Thread, long, UWSJob, JobOwner, HttpServletRequest)} will
+ * 		return 0)</li>
+ * </ul>
+ *
+ * <p>
+ * 	The strategy to use MUST be specified at creation using
+ * 	{@link #UserLimitedBlockingPolicy(long, int, boolean)} with a third
+ * 	parameter set to <code>true</code> to unblock the oldest thread if needed,
+ * 	or <code>false</code> to prevent blocking if the limit is reached.
+ * </p>
+ *
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 4.3 (11/2017)
+ * @since 4.3
+ */
+public class UserLimitedBlockingPolicy extends LimitedBlockingPolicy {
+
+	/** Default number of allowed blocked threads. */
+	public final static int DEFAULT_NB_MAX_BLOCKED = 3;
+
+	/** The maximum number of blocked threads for a given job and user. */
+	protected final int maxBlockedThreadsByUser;
+
+	/** List of all blocked threads.
+	 * <p>
+	 * 	Keys are an ID identifying a given job AND a given user
+	 * 	(basically: <code>jobId+";"+userId</code> ;  see
+	 * 	{@link #buildKey(UWSJob, JobOwner, HttpServletRequest)} for more
+	 * 	details).
+	 * </p>
+	 * <p>
+	 * 	Values are fixed-length queues of blocked threads.
+	 * </p> */
+	protected final Map<String,BlockingQueue<Thread>> blockedThreads;
+
+	/** Indicate what should happen when the maximum number of threads for a
+	 * given job and user is reached.
+	 * <p>
+	 * 	<code>true</code> to unblock the oldest blocked thread in order to allow
+	 * 	                  the new blocking.
+	 * 	<code>false</code> to forbid new blocking.
+	 * </p> */
+	protected final boolean unblockOld;
+
+	/**
+	 * Build a default {@link UserLimitedBlockingPolicy}.
+	 *
+	 * <p>
+	 * 	This instance will limit the number of blocked threads per user and job
+	 * 	to the default value (i.e. {@value #DEFAULT_NB_MAX_BLOCKED}) and will
+	 * 	limit the blocking duration to the default timeout
+	 * 	(see {@link LimitedBlockingPolicy#DEFAULT_TIMEOUT}).
+	 * </p>
+	 *
+	 * <p>
+	 * 	When the limit of threads is reached, the oldest thread is unblocked
+	 * 	in order to allow the new incoming blocking.
+	 * </p>
+	 */
+	public UserLimitedBlockingPolicy(){
+		this(DEFAULT_TIMEOUT, DEFAULT_NB_MAX_BLOCKED);
+	}
+
+	/**
+	 * Build a {@link UserLimitedBlockingPolicy} which will limit the blocking
+	 * duration to the given value and will limit the number of blocked threads
+	 * per job and user to the default value (i.e.
+	 * {@value #DEFAULT_NB_MAX_BLOCKED}).
+	 *
+	 * <p>
+	 * 	When the limit of threads is reached, the oldest thread is unblocked
+	 * 	in order to allow the new incoming blocking.
+	 * </p>
+	 *
+	 * @param timeout	Maximum blocking duration (in seconds).
+	 *               	<i>If &lt; 0, the default timeout (see
+	 *               	{@link LimitedBlockingPolicy#DEFAULT_TIMEOUT}) will be
+	 *               	set.</i>
+	 *
+	 * @see LimitedBlockingPolicy#LimitedBlockingPolicy(long)
+	 */
+	public UserLimitedBlockingPolicy(final long timeout){
+		this(timeout, DEFAULT_NB_MAX_BLOCKED);
+	}
+
+	/**
+	 * Build a {@link UserLimitedBlockingPolicy} which will limit the blocking
+	 * duration to the given value and will limit the number of blocked threads
+	 * per job and user to the given value.
+	 *
+	 * <p>
+	 * 	When the limit of threads is reached, the oldest thread is unblocked in
+	 * 	order to allow the new incoming blocking.
+	 * </p>
+	 *
+	 * @param timeout		Maximum blocking duration (in seconds).
+	 *               		<i>If &lt; 0, the default timeout (see
+	 *               		{@link LimitedBlockingPolicy#DEFAULT_TIMEOUT}) will
+	 *               		be set.</i>
+	 * @param maxNbBlocked	Maximum number of blocked threads allowed for a
+	 *                    	given job and a given user.
+	 *                    	<i>If &le; 0, this parameter will be ignored and the
+	 *                    	default value (i.e. {@value #DEFAULT_NB_MAX_BLOCKED})
+	 *                    	will be set instead.</i>
+	 */
+	public UserLimitedBlockingPolicy(final long timeout, final int maxNbBlocked){
+		this(timeout, maxNbBlocked, true);
+	}
+
+	/**
+	 * Build a {@link UserLimitedBlockingPolicy} which will limit the blocking
+	 * duration to the given value and will limit the number of blocked threads
+	 * per job and user to the given value.
+	 *
+	 * <p>
+	 * 	When the limit of threads is reached, the oldest thread is unblocked if
+	 * 	the 3rd parameter is <code>true</code>, or new incoming blocking will
+	 * 	be forbidden if this parameter is <code>false</code>.
+	 * </p>
+	 *
+	 * @param timeout		Maximum blocking duration (in seconds).
+	 *               		<i>If &lt; 0, the default timeout (see
+	 *               		{@link LimitedBlockingPolicy#DEFAULT_TIMEOUT}) will
+	 *               		be set.</i>
+	 * @param maxNbBlocked	Maximum number of blocked threads allowed for a
+	 *                    	given job and a given user.
+	 *                    	<i>If &le; 0, this parameter will be ignored and the
+	 *                    	default value (i.e. {@value #DEFAULT_NB_MAX_BLOCKED})
+	 *                    	will be set instead.</i>
+	 * @param unblockOld	Set the behavior to adopt when the maximum number of
+	 *                  	threads is reached for a given job and user.
+	 *                  	<code>true</code> to unblock the oldest thread in
+	 *                  	order to allow the new incoming blocking,
+	 *                  	<code>false</code> to forbid the new incoming
+	 *                  	blocking.
+	 */
+	public UserLimitedBlockingPolicy(final long timeout, final int maxNbBlocked, final boolean unblockOld){
+		super(timeout);
+		maxBlockedThreadsByUser = (maxNbBlocked <= 0) ? DEFAULT_NB_MAX_BLOCKED : maxNbBlocked;
+		blockedThreads = Collections.synchronizedMap(new HashMap<String,BlockingQueue<Thread>>());
+		this.unblockOld = unblockOld;
+	}
+
+	/**
+	 * Build the key for the map {@link #blockedThreads}.
+	 *
+	 * <p>The built key is: <code>jobId + ";" + userId</code>.</p>
+	 *
+	 * <p><i>Note:
+	 * 	If no user is logged in or if the user is not specified here or if it
+	 * 	does not have any ID, the IP address of the HTTP client will be used
+	 * 	instead.
+	 * </i></p>
+	 *
+	 * @param job		Job associated with the request to block.
+	 *           		<b>MUST NOT be NULL.</b>
+	 * @param user		User who asked the blocking behavior.
+	 *            		<i>If NULL (or it has a NULL ID), the IP address of the
+	 *            		HTTP client will be used.</i>
+	 * @param request	HTTP request which should be blocked.
+	 *               	<i>SHOULD NOT be NULL.</i>
+	 *
+	 * @return	The corresponding map key.
+	 *        	<i>NEVER NULL.</i>
+	 */
+	protected final String buildKey(final UWSJob job, final JobOwner user, final HttpServletRequest request){
+		if (user == null || user.getID() == null){
+			if (request == null)
+				return job.getJobId() + ";???";
+			else
+				return job.getJobId() + ";" + request.getRemoteAddr();
+		}else
+			return job.getJobId() + ";" + user.getID();
+	}
+
+	@Override
+	public long block(final Thread thread, final long userDuration, final UWSJob job, final JobOwner user, final HttpServletRequest request){
+		// Nothing should happen if no thread and/or no job is provided:
+		if (job == null || thread == null)
+			return 0;
+
+		// Get the ID of the blocking (job+user):
+		String id = buildKey(job, user, request);
+
+		// Get the corresponding queue (if any):
+		BlockingQueue<Thread> queue = blockedThreads.get(id);
+		if (queue == null)
+			queue = new ArrayBlockingQueue<Thread>(maxBlockedThreadsByUser);
+
+		// Try to add the recently blocked thread:
+		if (!queue.offer(thread)){
+			/* If it fails, 2 strategies are possible: */
+			/* 1/ Unblock the oldest blocked thread and add the given thread
+			 *    into the queue: */
+			if (unblockOld){
+				// Get the oldest blocked thread:
+				Thread old = queue.poll();
+				// Wake it up // Unblock it:
+				if (old != null){
+					synchronized(old){
+						old.notifyAll();
+					}
+				}
+				// Add the thread into the queue:
+				queue.offer(thread);
+			}
+			/* 2/ The given thread CAN NOT be blocked because too many threads
+			 *    for this job and user are already blocked => unblock it! */
+			else
+				return 0;
+		}
+
+		// Add the queue into the map:
+		blockedThreads.put(id, queue);
+
+		// Return the eventually limited duration to wait:
+		return super.block(thread, userDuration, job, user, request);
+
+	}
+
+	@Override
+	public void unblocked(final Thread unblockedThread, final UWSJob job, final JobOwner user, final HttpServletRequest request){
+		// Nothing should happen if no thread and/or no job is provided:
+		if (job == null || unblockedThread == null)
+			return;
+
+		// Get the ID of the blocking (job+user):
+		String id = buildKey(job, user, request);
+
+		// Get the corresponding queue (if any):
+		BlockingQueue<Thread> queue = blockedThreads.get(id);
+
+		if (queue != null){
+			Iterator<Thread> it = queue.iterator();
+			// Search for the corresponding item inside the queue:
+			while(it.hasNext()){
+				// When found...
+				if (it.next().equals(unblockedThread)){
+					// ...remove it from the queue:
+					it.remove();
+					// If the queue is now empty, remove the queue from the map:
+					if (queue.isEmpty())
+						blockedThreads.remove(id);
+					return;
+				}
+			}
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/uws/service/wait/WaitObserver.java b/src/uws/service/wait/WaitObserver.java
new file mode 100644
index 0000000..a6a3ac3
--- /dev/null
+++ b/src/uws/service/wait/WaitObserver.java
@@ -0,0 +1,60 @@
+package uws.service.wait;
+
+/*
+ * This file is part of UWSLibrary.
+ *
+ * UWSLibrary is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * UWSLibrary is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * 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 2017 - Astronomisches Rechen Institut (ARI)
+ */
+
+import uws.UWSException;
+import uws.job.ExecutionPhase;
+import uws.job.JobObserver;
+import uws.job.UWSJob;
+
+/**
+ * Job observer that unblock (here: notify) the given thread when a change of
+ * the execution phase is detected.
+ *
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 4.3 (11/2017)
+ * @since 4.3
+ */
+public class WaitObserver implements JobObserver {
+	private static final long serialVersionUID = 1L;
+
+	/** Thread to notify in case an execution phase occurs. */
+	private final Thread waitingThread;
+
+	/**
+	 * Build a {@link JobObserver} which will wake up the given thread when the
+	 * execution phase of watched jobs changes.
+	 *
+	 * @param thread	Thread to notify.
+	 */
+	public WaitObserver(final Thread thread){
+		waitingThread = thread;
+	}
+
+	@Override
+	public void update(final UWSJob job, final ExecutionPhase oldPhase, final ExecutionPhase newPhase) throws UWSException{
+		if (oldPhase != null && newPhase != null && oldPhase != newPhase){
+			synchronized(waitingThread){
+				waitingThread.notify();
+			}
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/test/uws/service/actions/TestJobSummary.java b/test/uws/service/actions/TestJobSummary.java
new file mode 100644
index 0000000..e5a4fd5
--- /dev/null
+++ b/test/uws/service/actions/TestJobSummary.java
@@ -0,0 +1,645 @@
+package uws.service.actions;
+
+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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.Part;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import uws.UWSException;
+import uws.job.ExecutionPhase;
+import uws.job.UWSJob;
+import uws.job.parameters.UWSParameters;
+import uws.job.user.JobOwner;
+import uws.service.wait.BlockingPolicy;
+import uws.service.wait.LimitedBlockingPolicy;
+
+public class TestJobSummary {
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@Test
+	public void testBlock(){
+		long waitingDuration = 1;
+
+		/* **************************************** */
+		/* NO BLOCKING POLICY (=> default behavior) */
+
+		UWSJob job = new UWSJob(new UWSParameters());
+		TestHttpServletRequest req = new TestHttpServletRequest();
+		req.addParams("WAIT", "" + waitingDuration);
+
+		// Without request and/or job => No blocking should occur:
+		TestThread t = new TestThread(null, null, null, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+		t = new TestThread(null, req, null, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+		t = new TestThread(null, null, job, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+
+		// No WAIT value or a not legal value (not integer) => no blocking:
+		req.clearParams();
+		t = new TestThread(null, req, job, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+		req.addParams("WAIT", "");
+		t = new TestThread(null, req, job, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+		req.clearParams();
+		req.addParams("WAIT", "foo");
+		t = new TestThread(null, req, job, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+
+		// With a job not in an "active" phase => No blocking ; immediate return:
+		req.clearParams();
+		req.addParams("WAIT", "" + waitingDuration);
+		ExecutionPhase[] nonActivePhases = new ExecutionPhase[]{ExecutionPhase.COMPLETED,ExecutionPhase.ABORTED,ExecutionPhase.ERROR,ExecutionPhase.ARCHIVED,ExecutionPhase.HELD,ExecutionPhase.SUSPENDED,ExecutionPhase.UNKNOWN};
+		for(ExecutionPhase p : nonActivePhases){
+			try{
+				job.setPhase(p, true);
+			}catch(UWSException ue){
+				fail("No error should occur when forcing a phase modification!");
+			}
+			t = new TestThread(null, req, job, null);
+			t.start();
+			waitFor(t);
+			assertFalse(t.isAlive());
+			assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+		}
+
+		// With a job in one of the "active" phases:
+		ExecutionPhase[] activePhases = new ExecutionPhase[]{ExecutionPhase.PENDING,ExecutionPhase.QUEUED,ExecutionPhase.EXECUTING};
+		for(ExecutionPhase p : activePhases){
+			try{
+				job.setPhase(p, true);
+			}catch(UWSException ue){
+				fail("No error should occur when forcing a phase modification!");
+			}
+			t = new TestThread(null, req, job, null);
+			t.start();
+			waitFor(t);
+			assertFalse(t.isAlive());
+			assertEquals(waitingDuration, t.getTime());
+		}
+
+		// With a PHASE parameter:
+		req.addParams("PHASE", "EXECUTING");
+		activePhases = new ExecutionPhase[]{ExecutionPhase.PENDING,ExecutionPhase.QUEUED};
+		for(ExecutionPhase p : activePhases){
+			try{
+				job.setPhase(p, true);
+			}catch(UWSException ue){
+				fail("No error should occur when forcing a phase modification!");
+			}
+			t = new TestThread(null, req, job, null);
+			t.start();
+			waitFor(t);
+			assertFalse(t.isAlive());
+			assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+		}
+		try{
+			job.setPhase(ExecutionPhase.EXECUTING, true);
+		}catch(UWSException ue){
+			fail("No error should occur when forcing a phase modification!");
+		}
+		t = new TestThread(null, req, job, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertEquals(waitingDuration, t.getTime());
+
+		// With several WAIT and PHASE parameters:
+		waitingDuration = 2;
+		req.addParams("wait", "" + waitingDuration);
+		req.addParams("PHASE", "PENDING");
+		activePhases = new ExecutionPhase[]{ExecutionPhase.EXECUTING,ExecutionPhase.QUEUED};
+		for(ExecutionPhase p : activePhases){
+			try{
+				job.setPhase(p, true);
+			}catch(UWSException ue){
+				fail("No error should occur when forcing a phase modification!");
+			}
+			t = new TestThread(null, req, job, null);
+			t.start();
+			waitFor(t);
+			assertFalse(t.isAlive());
+			assertTrue(t.getTime() >= 0 && t.getTime() < 1);
+		}
+		try{
+			job.setPhase(ExecutionPhase.PENDING, true);
+		}catch(UWSException ue){
+			fail("No error should occur when forcing a phase modification!");
+		}
+		t = new TestThread(null, req, job, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertEquals(1, t.getTime());
+
+		// With several WAIT parameters, including a negative one:
+		req.clearParams();
+		req.addParams("Wait", "-10");
+		req.addParams("wait", "1");
+		req.addParams("WAIT", "5");
+		try{
+			job.setPhase(ExecutionPhase.PENDING, true);
+		}catch(UWSException ue){
+			fail("No error should occur when forcing a phase modification!");
+		}
+		t = new TestThread(null, req, job, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertEquals(1, t.getTime());
+
+		/* ********************** */
+		/* WITH A BLOCKING POLICY */
+		req.clearParams();
+		req.addParams("wait", "" + waitingDuration);
+		long policyDuration = 1;
+		try{
+			job.setPhase(ExecutionPhase.EXECUTING, true);
+		}catch(UWSException ue){
+			fail("No error should occur when forcing a phase modification!");
+		}
+		t = new TestThread(new LimitedBlockingPolicy(policyDuration), req, job, null);
+		t.start();
+		waitFor(t);
+		assertFalse(t.isAlive());
+		assertEquals(policyDuration, t.getTime());
+	}
+
+	protected final void waitALittle(){
+		synchronized(this){
+			try{
+				Thread.sleep(10);
+			}catch(InterruptedException e){
+				e.printStackTrace();
+			}
+		}
+	}
+
+	protected final void waitFor(final Thread t){
+		synchronized(this){
+			try{
+				t.join();
+			}catch(InterruptedException e){
+				e.printStackTrace();
+			}
+		}
+	}
+
+	protected static final class TestThread extends Thread {
+
+		private final BlockingPolicy policy;
+		private final HttpServletRequest req;
+		private final UWSJob job;
+		private final JobOwner user;
+
+		public long start = -1;
+		public long end = -1;
+
+		public TestThread(final BlockingPolicy policy, final HttpServletRequest req, final UWSJob job, final JobOwner user){
+			this.policy = policy;
+			this.req = req;
+			this.job = job;
+			this.user = user;
+		}
+
+		@Override
+		public void run(){
+			start = System.currentTimeMillis();
+			JobSummary.block(policy, req, job, user);
+			end = System.currentTimeMillis();
+		}
+
+		/**
+		 * Get execution duration in seconds.
+		 *
+		 * @return	Execution duration of this thread in seconds
+		 *        	or -1 if still alive.
+		 */
+		public final long getTime(){
+			return (isAlive() || end > -1) ? (end - start) / 1000 : -1;
+		}
+
+	}
+
+	protected final static class TestHttpServletRequest implements HttpServletRequest {
+
+		private HashMap<String,String[]> parameters = new HashMap<String,String[]>();
+
+		private static class NamesEnumeration implements Enumeration<String> {
+
+			private final Iterator<String> it;
+
+			public NamesEnumeration(final Set<String> names){
+				this.it = names.iterator();
+			}
+
+			@Override
+			public boolean hasMoreElements(){
+				return it.hasNext();
+			}
+
+			@Override
+			public String nextElement(){
+				return it.next();
+			}
+
+		}
+
+		public void addParams(final String name, final String value){
+			if (parameters.containsKey(name)){
+				String[] values = parameters.get(name);
+				values = Arrays.copyOf(values, values.length + 1);
+				values[values.length - 1] = value;
+				parameters.put(name, values);
+			}else
+				parameters.put(name, new String[]{value});
+		}
+
+		public void clearParams(){
+			parameters.clear();
+		}
+
+		@Override
+		public Enumeration<String> getParameterNames(){
+			return new NamesEnumeration(parameters.keySet());
+		}
+
+		@Override
+		public String[] getParameterValues(String name){
+			return parameters.get(name);
+		}
+
+		@Override
+		public Map<String,String[]> getParameterMap(){
+			return parameters;
+		}
+
+		@Override
+		public String getParameter(String name){
+			String[] values = parameters.get(name);
+			if (values == null || values.length == 0)
+				return null;
+			else
+				return values[0];
+		}
+
+		@Override
+		public AsyncContext startAsync(ServletRequest arg0, ServletResponse arg1){
+			return null;
+		}
+
+		@Override
+		public AsyncContext startAsync(){
+			return null;
+		}
+
+		@Override
+		public void setCharacterEncoding(String arg0) throws UnsupportedEncodingException{
+
+		}
+
+		@Override
+		public void setAttribute(String arg0, Object arg1){
+
+		}
+
+		@Override
+		public void removeAttribute(String arg0){
+
+		}
+
+		@Override
+		public boolean isSecure(){
+			return false;
+		}
+
+		@Override
+		public boolean isAsyncSupported(){
+			return false;
+		}
+
+		@Override
+		public boolean isAsyncStarted(){
+			return false;
+		}
+
+		@Override
+		public ServletContext getServletContext(){
+			return null;
+		}
+
+		@Override
+		public int getServerPort(){
+			return 0;
+		}
+
+		@Override
+		public String getServerName(){
+			return null;
+		}
+
+		@Override
+		public String getScheme(){
+			return null;
+		}
+
+		@Override
+		public RequestDispatcher getRequestDispatcher(String arg0){
+			return null;
+		}
+
+		@Override
+		public int getRemotePort(){
+			return 0;
+		}
+
+		@Override
+		public String getRemoteHost(){
+			return null;
+		}
+
+		@Override
+		public String getRemoteAddr(){
+			return null;
+		}
+
+		@Override
+		public String getRealPath(String arg0){
+			return null;
+		}
+
+		@Override
+		public BufferedReader getReader() throws IOException{
+			return null;
+		}
+
+		@Override
+		public String getProtocol(){
+			return null;
+		}
+
+		@Override
+		public Enumeration<Locale> getLocales(){
+			return null;
+		}
+
+		@Override
+		public Locale getLocale(){
+			return null;
+		}
+
+		@Override
+		public int getLocalPort(){
+			return 0;
+		}
+
+		@Override
+		public String getLocalName(){
+			return null;
+		}
+
+		@Override
+		public String getLocalAddr(){
+			return null;
+		}
+
+		@Override
+		public ServletInputStream getInputStream() throws IOException{
+			return null;
+		}
+
+		@Override
+		public DispatcherType getDispatcherType(){
+			return null;
+		}
+
+		@Override
+		public String getContentType(){
+			return null;
+		}
+
+		@Override
+		public int getContentLength(){
+			return 0;
+		}
+
+		@Override
+		public String getCharacterEncoding(){
+			return null;
+		}
+
+		@Override
+		public Enumeration<String> getAttributeNames(){
+			return null;
+		}
+
+		@Override
+		public Object getAttribute(String arg0){
+			return null;
+		}
+
+		@Override
+		public AsyncContext getAsyncContext(){
+			return null;
+		}
+
+		@Override
+		public void logout() throws ServletException{}
+
+		@Override
+		public void login(String arg0, String arg1) throws ServletException{}
+
+		@Override
+		public boolean isUserInRole(String arg0){
+			return false;
+		}
+
+		@Override
+		public boolean isRequestedSessionIdValid(){
+			return false;
+		}
+
+		@Override
+		public boolean isRequestedSessionIdFromUrl(){
+			return false;
+		}
+
+		@Override
+		public boolean isRequestedSessionIdFromURL(){
+			return false;
+		}
+
+		@Override
+		public boolean isRequestedSessionIdFromCookie(){
+			return false;
+		}
+
+		@Override
+		public Principal getUserPrincipal(){
+			return null;
+		}
+
+		@Override
+		public HttpSession getSession(boolean arg0){
+			return null;
+		}
+
+		@Override
+		public HttpSession getSession(){
+			return null;
+		}
+
+		@Override
+		public String getServletPath(){
+			return null;
+		}
+
+		@Override
+		public String getRequestedSessionId(){
+			return null;
+		}
+
+		@Override
+		public StringBuffer getRequestURL(){
+			return null;
+		}
+
+		@Override
+		public String getRequestURI(){
+			return null;
+		}
+
+		@Override
+		public String getRemoteUser(){
+			return null;
+		}
+
+		@Override
+		public String getQueryString(){
+			return null;
+		}
+
+		@Override
+		public String getPathTranslated(){
+			return null;
+		}
+
+		@Override
+		public String getPathInfo(){
+			return null;
+		}
+
+		@Override
+		public Collection<Part> getParts() throws IOException, IllegalStateException, ServletException{
+			return null;
+		}
+
+		@Override
+		public Part getPart(String arg0) throws IOException, IllegalStateException, ServletException{
+			return null;
+		}
+
+		@Override
+		public String getMethod(){
+			return "GET";
+		}
+
+		@Override
+		public int getIntHeader(String arg0){
+			return 0;
+		}
+
+		@Override
+		public Enumeration<String> getHeaders(String arg0){
+			return null;
+		}
+
+		@Override
+		public Enumeration<String> getHeaderNames(){
+			return null;
+		}
+
+		@Override
+		public String getHeader(String arg0){
+			return null;
+		}
+
+		@Override
+		public long getDateHeader(String arg0){
+			return 0;
+		}
+
+		@Override
+		public Cookie[] getCookies(){
+			return null;
+		}
+
+		@Override
+		public String getContextPath(){
+			return null;
+		}
+
+		@Override
+		public String getAuthType(){
+			return null;
+		}
+
+		@Override
+		public boolean authenticate(HttpServletResponse arg0) throws IOException, ServletException{
+			return false;
+		}
+
+	}
+
+}
\ No newline at end of file
diff --git a/test/uws/service/wait/TestLimitedBlockingPolicy.java b/test/uws/service/wait/TestLimitedBlockingPolicy.java
new file mode 100644
index 0000000..a44a6a8
--- /dev/null
+++ b/test/uws/service/wait/TestLimitedBlockingPolicy.java
@@ -0,0 +1,67 @@
+package uws.service.wait;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import uws.job.UWSJob;
+import uws.job.parameters.UWSParameters;
+
+public class TestLimitedBlockingPolicy {
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@Test
+	public void testLimitedBlockingPolicy(){
+		LimitedBlockingPolicy policy = new LimitedBlockingPolicy();
+		assertEquals(LimitedBlockingPolicy.DEFAULT_TIMEOUT, policy.timeout);
+	}
+
+	@Test
+	public void testLimitedBlockingPolicyLong(){
+		// Negative time => DEFAULT_TIMEOUT
+		LimitedBlockingPolicy policy = new LimitedBlockingPolicy(-1);
+		assertEquals(LimitedBlockingPolicy.DEFAULT_TIMEOUT, policy.timeout);
+
+		// Null time => 0 (meaning no blocking)
+		policy = new LimitedBlockingPolicy(0);
+		assertEquals(0, policy.timeout);
+
+		// A time LESS THAN the default one => the given time
+		policy = new LimitedBlockingPolicy(10);
+		assertEquals(10, policy.timeout);
+
+		// A time GREATER THAN the default one => the given time
+		policy = new LimitedBlockingPolicy(100);
+		assertEquals(100, policy.timeout);
+	}
+
+	@Test
+	public void testBlock(){
+		LimitedBlockingPolicy policy = new LimitedBlockingPolicy();
+		Thread thread = new Thread("1");
+		UWSJob testJob = new UWSJob(new UWSParameters());
+
+		// Nothing should happen if no job and/or thread:
+		assertEquals(0, policy.block(null, 10, null, null, null));
+		assertEquals(0, policy.block(thread, 10, null, null, null));
+		assertEquals(0, policy.block(null, 10, testJob, null, null));
+
+		// If no time is specified by the user (i.e. 0), 0 is set (meaning no blocking):
+		assertEquals(0, policy.block(thread, 0, testJob, null, null));
+
+		// If a negative time is specified by the user (meaning an unlimited waiting time), the default time is set:
+		assertEquals(policy.timeout, policy.block(thread, -1, testJob, null, null));
+
+		// If a positive time is specified by the user BUT LESS THAN the time set in the policy, the user time is set:
+		long userTime = policy.timeout - 10;
+		assertEquals(userTime, policy.block(thread, userTime, testJob, null, null));
+
+		// If a positive time is specified by the user BUT LESS THAN the time set in the policy, the user time is set:
+		userTime = policy.timeout + 10;
+		assertEquals(policy.timeout, policy.block(thread, userTime, testJob, null, null));
+	}
+
+}
\ No newline at end of file
diff --git a/test/uws/service/wait/TestUserLimitedBlockingPolicy.java b/test/uws/service/wait/TestUserLimitedBlockingPolicy.java
new file mode 100644
index 0000000..aaafb3b
--- /dev/null
+++ b/test/uws/service/wait/TestUserLimitedBlockingPolicy.java
@@ -0,0 +1,706 @@
+package uws.service.wait;
+
+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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.Part;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import uws.job.JobList;
+import uws.job.UWSJob;
+import uws.job.parameters.UWSParameters;
+import uws.job.user.JobOwner;
+
+public class TestUserLimitedBlockingPolicy {
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@Test
+	public void testUserLimitedBlockingPolicyLongInt(){
+		/* ****************************** */
+		/* NB MAX BLOCKED THREADS BY USER */
+
+		// Negative number of threads:
+		UserLimitedBlockingPolicy policy = new UserLimitedBlockingPolicy(10000, -1);
+		assertEquals(UserLimitedBlockingPolicy.DEFAULT_NB_MAX_BLOCKED, policy.maxBlockedThreadsByUser);
+
+		// Null number of threads:
+		policy = new UserLimitedBlockingPolicy(10000, 0);
+		assertEquals(UserLimitedBlockingPolicy.DEFAULT_NB_MAX_BLOCKED, policy.maxBlockedThreadsByUser);
+
+		// A positive number of threads LESS THAN DEFAULT_NB_MAX_BLOCKED:
+		policy = new UserLimitedBlockingPolicy(10000, 1);
+		assertEquals(1, policy.maxBlockedThreadsByUser);
+
+		// A positive number of threads GREATER THAN DEFAULT_NB_MAX_BLOCKED:
+		policy = new UserLimitedBlockingPolicy(10000, 10);
+		assertEquals(10, policy.maxBlockedThreadsByUser);
+	}
+
+	@Test
+	public void testBuildKey(){
+		UserLimitedBlockingPolicy policy = new UserLimitedBlockingPolicy();
+		UWSJob testJob = new UWSJob("123456", (new Date()).getTime(), null, new UWSParameters(), -1, -1, -1, null, null);
+
+		// With no job => ERROR!
+		try{
+			policy.buildKey(null, null, null);
+			fail("Impossible to generate a key without a job!");
+		}catch(NullPointerException npe){}
+
+		// With only a job => jobId + ";???"
+		assertEquals("123456;???", policy.buildKey(testJob, null, null));
+
+		// With a job and a user whose the ID is null => jobId + ";???"
+		assertEquals("123456;???", policy.buildKey(testJob, new TestUser(null), null));
+
+		// With a job and a user whose the ID is NOT null => jobId + ";" + UserID
+		assertEquals("123456;myID", policy.buildKey(testJob, new TestUser("myID"), null));
+
+		// With a job and a request => jobId + ";" + IPAddress
+		assertEquals("123456;1.2.3.4", policy.buildKey(testJob, null, new TestHttpServletRequest()));
+
+		// With a job, a request and a user whose the ID is null => jobId + ";" + IPAddress
+		assertEquals("123456;1.2.3.4", policy.buildKey(testJob, new TestUser(null), new TestHttpServletRequest()));
+	}
+
+	@Test
+	public void testBlock(){
+		/* ************************************* */
+		/* No Problem If No Job And/Or No Thread */
+
+		try{
+			UserLimitedBlockingPolicy policy = new UserLimitedBlockingPolicy();
+			assertEquals(0, policy.block(null, 10, null, null, null));
+			assertEquals(0, policy.block(new Thread("0"), 10, null, null, null));
+			assertEquals(0, policy.block(null, 10, new UWSJob(new UWSParameters()), null, null));
+		}catch(Exception ex){
+			ex.printStackTrace();
+			fail("Nothing should happen if no job and/or thread is asked to be blocked!");
+		}
+
+		JobOwner user = new TestUser("myId");
+		UWSJob testJob1 = new UWSJob("123456", (new Date()).getTime(), user, new UWSParameters(), -1, -1, -1, null, null);
+		UWSJob testJob2 = new UWSJob("654321", (new Date()).getTime(), user, new UWSParameters(), -1, -1, -1, null, null);
+		final String key1 = "123456;myId", key2 = "654321;myId";
+
+		/* *************************** */
+		/* NO OLD BLOCKING REPLACEMENT */
+
+		// Test with just one job and only one allowed access => OK!
+		UserLimitedBlockingPolicy policy = new UserLimitedBlockingPolicy(10, 1, false);
+		assertEquals(5, policy.block(new Thread("1"), 5, testJob1, user, null));
+		assertEquals(key1, policy.buildKey(testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(1, policy.blockedThreads.get(key1).size());
+		assertEquals("1", policy.blockedThreads.get(key1).peek().getName());
+		assertEquals(0, policy.blockedThreads.get(key1).remainingCapacity());
+
+		// Test with the same job and same user => Rejected!
+		assertEquals(0, policy.block(new Thread("2"), 5, testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(1, policy.blockedThreads.get(key1).size());
+		assertEquals("1", policy.blockedThreads.get(key1).peek().getName());
+		assertEquals(0, policy.blockedThreads.get(key1).remainingCapacity());
+
+		// Test with a second job => OK!
+		assertEquals(5, policy.block(new Thread("3"), 5, testJob2, user, null));
+		assertEquals(key2, policy.buildKey(testJob2, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key2));
+		assertEquals(1, policy.blockedThreads.get(key2).size());
+		assertEquals("3", policy.blockedThreads.get(key2).peek().getName());
+		assertEquals(0, policy.blockedThreads.get(key2).remainingCapacity());
+
+		/* ************************ */
+		/* OLD BLOCKING REPLACEMENT */
+
+		// 1st test with just one job and only one allowed access => OK!
+		policy = new UserLimitedBlockingPolicy(10, 1, true);
+		assertEquals(5, policy.block(new Thread("1"), 5, testJob1, user, null));
+		assertEquals(key1, policy.buildKey(testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(1, policy.blockedThreads.get(key1).size());
+		assertEquals("1", policy.blockedThreads.get(key1).peek().getName());
+		assertEquals(0, policy.blockedThreads.get(key1).remainingCapacity());
+
+		// 2nd test with the same job and same user => OK!
+		assertEquals(5, policy.block(new Thread("2"), 5, testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(1, policy.blockedThreads.get(key1).size());
+		assertEquals("2", policy.blockedThreads.get(key1).peek().getName());
+		assertEquals(0, policy.blockedThreads.get(key1).remainingCapacity());
+
+		// Test with a second job => OK!
+		assertEquals(5, policy.block(new Thread("3"), 5, testJob2, user, null));
+		assertEquals(key2, policy.buildKey(testJob2, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key2));
+		assertEquals(1, policy.blockedThreads.get(key2).size());
+		assertEquals("3", policy.blockedThreads.get(key2).peek().getName());
+		assertEquals(0, policy.blockedThreads.get(key2).remainingCapacity());
+
+		/* ************************************************************************* */
+		/* MORE THAN 1 OF CAPACITY (i.e. nb access for a given job and a given user) */
+
+		/* WITH NO old blocking replacement */
+
+		// 1st test with just one job and 2 allowed accesses => OK!
+		policy = new UserLimitedBlockingPolicy(10, 2, false);
+		assertEquals(5, policy.block(new Thread("1"), 5, testJob1, user, null));
+		assertEquals(key1, policy.buildKey(testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(1, policy.blockedThreads.get(key1).size());
+		assertEquals("1", policy.blockedThreads.get(key1).peek().getName());
+		assertEquals(1, policy.blockedThreads.get(key1).remainingCapacity());
+
+		// 2nd test with the same job and same user => OK!
+		assertEquals(5, policy.block(new Thread("2"), 5, testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(2, policy.blockedThreads.get(key1).size());
+		Iterator<Thread> it = policy.blockedThreads.get(key1).iterator();
+		assertEquals("1", it.next().getName());
+		assertEquals("2", it.next().getName());
+		assertEquals(0, policy.blockedThreads.get(key1).remainingCapacity());
+
+		// 3rd test with the same job and same user => Rejected!
+		assertEquals(0, policy.block(new Thread("3"), 5, testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(2, policy.blockedThreads.get(key1).size());
+		it = policy.blockedThreads.get(key1).iterator();
+		assertEquals("1", it.next().getName());
+		assertEquals("2", it.next().getName());
+		assertEquals(0, policy.blockedThreads.get(key1).remainingCapacity());
+
+		/* WITH old blocking replacement */
+
+		// 1st test with just one job and 2 allowed accesses => OK!
+		policy = new UserLimitedBlockingPolicy(10, 2, true);
+		assertEquals(5, policy.block(new Thread("1"), 5, testJob1, user, null));
+		assertEquals(key1, policy.buildKey(testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(1, policy.blockedThreads.get(key1).size());
+		assertEquals("1", policy.blockedThreads.get(key1).peek().getName());
+		assertEquals(1, policy.blockedThreads.get(key1).remainingCapacity());
+
+		// 2nd test with the same job and same user => OK!
+		assertEquals(5, policy.block(new Thread("2"), 5, testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(2, policy.blockedThreads.get(key1).size());
+		it = policy.blockedThreads.get(key1).iterator();
+		assertEquals("1", it.next().getName());
+		assertEquals("2", it.next().getName());
+		assertEquals(0, policy.blockedThreads.get(key1).remainingCapacity());
+
+		// 3rd test with the same job and same user => OK!
+		assertEquals(5, policy.block(new Thread("3"), 5, testJob1, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key1));
+		assertEquals(2, policy.blockedThreads.get(key1).size());
+		it = policy.blockedThreads.get(key1).iterator();
+		assertEquals("2", it.next().getName());
+		assertEquals("3", it.next().getName());
+		assertEquals(0, policy.blockedThreads.get(key1).remainingCapacity());
+	}
+
+	@Test
+	public void testUnblocked(){
+		JobOwner user = new TestUser("myId");
+		Thread thread1 = new Thread("1"), thread2 = new Thread("2");
+		UWSJob testJob = new UWSJob("123456", (new Date()).getTime(), user, new UWSParameters(), -1, -1, -1, null, null);
+		final String key = "123456;myId";
+
+		// Block 2 jobs:
+		UserLimitedBlockingPolicy policy = new UserLimitedBlockingPolicy(10, 2, false);
+		assertEquals(5, policy.block(thread1, 5, testJob, user, null));
+		assertEquals(5, policy.block(thread2, 5, testJob, user, null));
+		assertEquals(key, policy.buildKey(testJob, user, null));
+		assertTrue(policy.blockedThreads.containsKey(key));
+		assertEquals(2, policy.blockedThreads.get(key).size());
+		Iterator<Thread> it = policy.blockedThreads.get(key).iterator();
+		assertEquals("1", it.next().getName());
+		assertEquals("2", it.next().getName());
+		assertEquals(0, policy.blockedThreads.get(key).remainingCapacity());
+
+		// Unblock one:
+		policy.unblocked(thread1, testJob, user, null);
+		assertTrue(policy.blockedThreads.containsKey(key));
+		assertEquals(1, policy.blockedThreads.get(key).size());
+		assertEquals("2", policy.blockedThreads.get(key).peek().getName());
+		assertEquals(1, policy.blockedThreads.get(key).remainingCapacity());
+
+		// Unblock the second one:
+		policy.unblocked(thread2, testJob, user, null);
+		assertFalse(policy.blockedThreads.containsKey(key));
+
+		// Try unblocking a not-blocked thread:
+		try{
+			policy.unblocked(new Thread("3"), testJob, user, null);
+		}catch(Exception ex){
+			ex.printStackTrace();
+			fail("Nothing should happen if the given thread is not blocked!");
+		}
+
+		// Nothing should happen if no job and/or thread:
+		try{
+			policy.unblocked(null, null, null, null);
+			policy.unblocked(new Thread("0"), null, null, null);
+			policy.unblocked(null, new UWSJob(new UWSParameters()), null, null);
+		}catch(Exception ex){
+			ex.printStackTrace();
+			fail("Nothing should happen if no job and/or thread is asked to be unblocked!");
+		}
+	}
+
+	protected final static class TestUser implements JobOwner {
+
+		private final String id;
+
+		public TestUser(final String ID){
+			id = ID;
+		}
+
+		@Override
+		public String getID(){
+			return id;
+		}
+
+		@Override
+		public String getPseudo(){
+			return null;
+		}
+
+		@Override
+		public boolean hasReadPermission(JobList jl){
+			return false;
+		}
+
+		@Override
+		public boolean hasWritePermission(JobList jl){
+			return false;
+		}
+
+		@Override
+		public boolean hasReadPermission(UWSJob job){
+			return false;
+		}
+
+		@Override
+		public boolean hasWritePermission(UWSJob job){
+			return false;
+		}
+
+		@Override
+		public boolean hasExecutePermission(UWSJob job){
+			return false;
+		}
+
+		@Override
+		public Map<String,Object> getDataToSave(){
+			return null;
+		}
+
+		@Override
+		public void restoreData(Map<String,Object> data){}
+
+	}
+
+	protected final static class TestHttpServletRequest implements HttpServletRequest {
+
+		private HashMap<String,String[]> parameters = new HashMap<String,String[]>();
+
+		private static class NamesEnumeration implements Enumeration<String> {
+
+			private final Iterator<String> it;
+
+			public NamesEnumeration(final Set<String> names){
+				this.it = names.iterator();
+			}
+
+			@Override
+			public boolean hasMoreElements(){
+				return it.hasNext();
+			}
+
+			@Override
+			public String nextElement(){
+				return it.next();
+			}
+
+		}
+
+		@Override
+		public String getRemoteAddr(){
+			return "1.2.3.4";
+		}
+
+		public void addParams(final String name, final String value){
+			if (parameters.containsKey(name)){
+				String[] values = parameters.get(name);
+				values = Arrays.copyOf(values, values.length + 1);
+				values[values.length - 1] = value;
+				parameters.put(name, values);
+			}else
+				parameters.put(name, new String[]{value});
+		}
+
+		public void clearParams(){
+			parameters.clear();
+		}
+
+		@Override
+		public Enumeration<String> getParameterNames(){
+			return new NamesEnumeration(parameters.keySet());
+		}
+
+		@Override
+		public String[] getParameterValues(String name){
+			return parameters.get(name);
+		}
+
+		@Override
+		public Map<String,String[]> getParameterMap(){
+			return parameters;
+		}
+
+		@Override
+		public String getParameter(String name){
+			String[] values = parameters.get(name);
+			if (values == null || values.length == 0)
+				return null;
+			else
+				return values[0];
+		}
+
+		@Override
+		public AsyncContext startAsync(ServletRequest arg0, ServletResponse arg1){
+			return null;
+		}
+
+		@Override
+		public AsyncContext startAsync(){
+			return null;
+		}
+
+		@Override
+		public void setCharacterEncoding(String arg0) throws UnsupportedEncodingException{
+
+		}
+
+		@Override
+		public void setAttribute(String arg0, Object arg1){
+
+		}
+
+		@Override
+		public void removeAttribute(String arg0){
+
+		}
+
+		@Override
+		public boolean isSecure(){
+			return false;
+		}
+
+		@Override
+		public boolean isAsyncSupported(){
+			return false;
+		}
+
+		@Override
+		public boolean isAsyncStarted(){
+			return false;
+		}
+
+		@Override
+		public ServletContext getServletContext(){
+			return null;
+		}
+
+		@Override
+		public int getServerPort(){
+			return 0;
+		}
+
+		@Override
+		public String getServerName(){
+			return null;
+		}
+
+		@Override
+		public String getScheme(){
+			return null;
+		}
+
+		@Override
+		public RequestDispatcher getRequestDispatcher(String arg0){
+			return null;
+		}
+
+		@Override
+		public int getRemotePort(){
+			return 0;
+		}
+
+		@Override
+		public String getRemoteHost(){
+			return null;
+		}
+
+		@Override
+		public String getRealPath(String arg0){
+			return null;
+		}
+
+		@Override
+		public BufferedReader getReader() throws IOException{
+			return null;
+		}
+
+		@Override
+		public String getProtocol(){
+			return null;
+		}
+
+		@Override
+		public Enumeration<Locale> getLocales(){
+			return null;
+		}
+
+		@Override
+		public Locale getLocale(){
+			return null;
+		}
+
+		@Override
+		public int getLocalPort(){
+			return 0;
+		}
+
+		@Override
+		public String getLocalName(){
+			return null;
+		}
+
+		@Override
+		public String getLocalAddr(){
+			return null;
+		}
+
+		@Override
+		public ServletInputStream getInputStream() throws IOException{
+			return null;
+		}
+
+		@Override
+		public DispatcherType getDispatcherType(){
+			return null;
+		}
+
+		@Override
+		public String getContentType(){
+			return null;
+		}
+
+		@Override
+		public int getContentLength(){
+			return 0;
+		}
+
+		@Override
+		public String getCharacterEncoding(){
+			return null;
+		}
+
+		@Override
+		public Enumeration<String> getAttributeNames(){
+			return null;
+		}
+
+		@Override
+		public Object getAttribute(String arg0){
+			return null;
+		}
+
+		@Override
+		public AsyncContext getAsyncContext(){
+			return null;
+		}
+
+		@Override
+		public void logout() throws ServletException{}
+
+		@Override
+		public void login(String arg0, String arg1) throws ServletException{}
+
+		@Override
+		public boolean isUserInRole(String arg0){
+			return false;
+		}
+
+		@Override
+		public boolean isRequestedSessionIdValid(){
+			return false;
+		}
+
+		@Override
+		public boolean isRequestedSessionIdFromUrl(){
+			return false;
+		}
+
+		@Override
+		public boolean isRequestedSessionIdFromURL(){
+			return false;
+		}
+
+		@Override
+		public boolean isRequestedSessionIdFromCookie(){
+			return false;
+		}
+
+		@Override
+		public Principal getUserPrincipal(){
+			return null;
+		}
+
+		@Override
+		public HttpSession getSession(boolean arg0){
+			return null;
+		}
+
+		@Override
+		public HttpSession getSession(){
+			return null;
+		}
+
+		@Override
+		public String getServletPath(){
+			return null;
+		}
+
+		@Override
+		public String getRequestedSessionId(){
+			return null;
+		}
+
+		@Override
+		public StringBuffer getRequestURL(){
+			return null;
+		}
+
+		@Override
+		public String getRequestURI(){
+			return null;
+		}
+
+		@Override
+		public String getRemoteUser(){
+			return null;
+		}
+
+		@Override
+		public String getQueryString(){
+			return null;
+		}
+
+		@Override
+		public String getPathTranslated(){
+			return null;
+		}
+
+		@Override
+		public String getPathInfo(){
+			return null;
+		}
+
+		@Override
+		public Collection<Part> getParts() throws IOException, IllegalStateException, ServletException{
+			return null;
+		}
+
+		@Override
+		public Part getPart(String arg0) throws IOException, IllegalStateException, ServletException{
+			return null;
+		}
+
+		@Override
+		public String getMethod(){
+			return "GET";
+		}
+
+		@Override
+		public int getIntHeader(String arg0){
+			return 0;
+		}
+
+		@Override
+		public Enumeration<String> getHeaders(String arg0){
+			return null;
+		}
+
+		@Override
+		public Enumeration<String> getHeaderNames(){
+			return null;
+		}
+
+		@Override
+		public String getHeader(String arg0){
+			return null;
+		}
+
+		@Override
+		public long getDateHeader(String arg0){
+			return 0;
+		}
+
+		@Override
+		public Cookie[] getCookies(){
+			return null;
+		}
+
+		@Override
+		public String getContextPath(){
+			return null;
+		}
+
+		@Override
+		public String getAuthType(){
+			return null;
+		}
+
+		@Override
+		public boolean authenticate(HttpServletResponse arg0) throws IOException, ServletException{
+			return false;
+		}
+
+	}
+
+}
\ No newline at end of file
diff --git a/test/uws/service/wait/TestWaitObserver.java b/test/uws/service/wait/TestWaitObserver.java
new file mode 100644
index 0000000..e1529fc
--- /dev/null
+++ b/test/uws/service/wait/TestWaitObserver.java
@@ -0,0 +1,77 @@
+package uws.service.wait;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import uws.UWSException;
+import uws.job.ExecutionPhase;
+
+public class TestWaitObserver {
+
+	@Before
+	public void setUp() throws Exception{}
+
+	protected final void waitALittle(){
+		synchronized(this){
+			try{
+				Thread.sleep(10);
+			}catch(InterruptedException e){
+				e.printStackTrace();
+			}
+		}
+	}
+
+	@Test
+	public void testUpdate(){
+		Thread thread = new TestThread();
+		WaitObserver obs = new WaitObserver(thread);
+
+		thread.start();
+
+		try{
+			// No phase => Thread still blocked!
+			obs.update(null, null, null);
+			waitALittle();
+			assertTrue(thread.isAlive());
+			obs.update(null, ExecutionPhase.PENDING, null);
+			waitALittle();
+			assertTrue(thread.isAlive());
+			obs.update(null, null, ExecutionPhase.PENDING);
+			waitALittle();
+			assertTrue(thread.isAlive());
+
+			// Same phase => Thread still blocked!
+			obs.update(null, ExecutionPhase.PENDING, ExecutionPhase.PENDING);
+			waitALittle();
+			assertTrue(thread.isAlive());
+
+			// Different phase => Thread UNblocked!
+			obs.update(null, ExecutionPhase.PENDING, ExecutionPhase.EXECUTING);
+			waitALittle();
+			assertFalse(thread.isAlive());
+		}catch(UWSException e){
+			e.printStackTrace();
+			fail("No error should have happened while calling WaitObserver.update()!");
+		}
+
+	}
+
+	protected final static class TestThread extends Thread {
+		@Override
+		public void run(){
+			synchronized(this){
+				try{
+					wait();
+				}catch(InterruptedException e){
+					e.printStackTrace();
+				}
+			}
+		}
+
+	}
+
+}
\ No newline at end of file
-- 
GitLab