diff --git a/src/uws/service/UWSService.java b/src/uws/service/UWSService.java index c35cad94049d4cb4ec94b111d8b13cd82b5881f8..600cf0287ffc2910fbc3b7e4efcf4f07c4533693 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é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 e2015fb55964658945e09c3be2e7f67bfd32edbf..ebbde879ce2dd59affa044b7e61d62805f640da6 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é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 e35e1d80cab2d67f234f2d6044c2d0c467e47fc0..89acba1bba92383a20f16609fac77bed818be489 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é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 0000000000000000000000000000000000000000..0389f77d9fc65ba90145e69e2171c473dfe0709e --- /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é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 < 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 > 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>< 0 means indefinite, 0 means no wait and + * > 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>< 0 means indefinite, 0 means no wait and > 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 0000000000000000000000000000000000000000..82ff7090c825291df394b62a0e98f8df0029b088 --- /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é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 < 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 0000000000000000000000000000000000000000..5b875ba55ebf7153256c6d14e9110108bafd70a8 --- /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é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 < 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 < 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 ≤ 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 < 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 ≤ 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 0000000000000000000000000000000000000000..a6a3ac34c390a9e52f100bab9651c6e6b8898078 --- /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é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 0000000000000000000000000000000000000000..e5a4fd5fa283dfc1e7abc3443f7c622474654c68 --- /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 0000000000000000000000000000000000000000..a44a6a80c0f0b54eeb97e2de92175315568d42cf --- /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 0000000000000000000000000000000000000000..aaafb3b0a8eb8c0bc9b4c5452c098fbf0f55358e --- /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 0000000000000000000000000000000000000000..e1529fc606d8e198475a5f119fe6dd55b7840f4b --- /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