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
An abstract facility to implement the Universal Worker Service pattern.
* *It can manage several jobs lists (create new, get and remove).
* *It also interprets {@link HttpServletRequest}, applies the action specified in its given URL and parameters * (according to the IVOA Proposed Recommendation of 2010-02-10) * and returns the corresponding response in a {@link HttpServletResponse}.
* *Any subclass of {@link UWSService} has one object called the UWS URL interpreter. It is stored in the field {@link #urlInterpreter}. * It lets interpreting the URL of any received request. Thus you can know on which jobs list, job and/or job attribute(s) * the request applies.
* *This interpreter must be initialized with the base URL/URI of this UWS. By using the default constructor (the one with no parameter), * the URL interpreter will be built at the first request (see {@link UWSUrl#UWSUrl(HttpServletRequest)}) and so the base URI is * extracted directly from the request).
* *You want to set another base URI or to use a custom URL interpreter, you have to set yourself the interpreter * by using the method {@link #setUrlInterpreter(UWSUrl)}.
* *The most important abstract function of this class is {@link UWSService#createJob(Map)}. It allows to create an instance * of the type of job which is managed by this UWS. The only parameter is a map of a job attributes. It is the same map that * take the functions {@link UWSJob#UWSJob(Map)} and {@link UWSJob#addOrUpdateParameters(Map)}.
* *There are two convenient implementations of this abstract method in {@link BasicUWS} and {@link ExtendedUWS}. These two implementations * are based on the Java Reflection.
* *All the actions described in the IVOA recommendation are already managed. Each of these actions are defined in * an instance of {@link UWSAction}:
*However you can add your own UWS actions ! To do that you just need to implement the abstract class {@link UWSAction} * and to call the method {@link #addUWSAction(UWSAction)} with an instance of this implementation.
* *IMPORTANT: You must be careful when you override the function {@link UWSAction#match(UWSUrl, String, HttpServletRequest)}
* so that your test is as precise as possible ! Indeed the order in which the actions of a UWS are evaluated is very important !
* If you want to be sure your custom UWS action is always evaluated before any other UWS action you can use the function
* {@link #addUWSAction(int, UWSAction)} with 0 as first parameter !
Note: You can also replace an existing UWS action thanks to the method {@link #replaceUWSAction(UWSAction)} or * {@link #setUWSAction(int, UWSAction)} !
* *Some UWS actions need to know the current user so that they can adapt their response (i.e. LIST_JOBS must return the jobs of only * one user: the current one). Thus, before executing a UWS action (and even before choosing the good action in function of the request) * the function {@link UserIdentifier#extractUserId(UWSUrl, HttpServletRequest)} is called. Its goal * is to identify the current user in function of the received request.
* *
* Notes:
*
*
*
One of the goals of a UWS is to manage an execution queue for all managed jobs. This task is given to an instance * of {@link DefaultExecutionManager}, stored in the field {@link #executionManager}. Each time a job is created, * the UWS sets it the execution manager (see {@link AddJob}). Thus the {@link UWSJob#start()} method will ask to the manager * whether it can execute now or whether it must be put in a {@link ExecutionPhase#QUEUED QUEUED} phase until enough resources are available for its execution.
* *By extending the class {@link DefaultExecutionManager} and by overriding {@link DefaultExecutionManager#isReadyForExecution(UWSJob)} * you can change the condition which puts a job in the {@link ExecutionPhase#EXECUTING EXECUTING} or in the {@link ExecutionPhase#QUEUED QUEUED} phase. By default, a job is put * in the {@link ExecutionPhase#QUEUED QUEUED} phase if there are more running jobs than a given number.
* *With this manager it is also possible to list all running jobs in addition of all queued jobs, thanks to the methods: * {@link DefaultExecutionManager#getRunningJobs()}, {@link DefaultExecutionManager#getQueuedJobs()}, {@link DefaultExecutionManager#getNbRunningJobs()} * and {@link DefaultExecutionManager#getNbQueuedJobs()}.
* *According to the IVOA recommendation, the XML format is the default format in which each UWS resource must be returned. However it * is told that other formats can also be managed. To allow that, {@link UWSService} manages a list of {@link UWSSerializer} and * lets define which is the default one to use. By default, there are two serializers: {@link XMLSerializer} (the default choice) * and {@link JSONSerializer}.
* *One proposed way to choose automatically the format to use is to look at the Accept header of a HTTP-Request. This header field is * a list of MIME types (each one with a quality - a sort of priority). Thus each {@link UWSSerializer} is associated with a MIME type so * that {@link UWSService} can choose automatically the preferred format and so, the serializer to use.
* *WARNING: Only one {@link UWSSerializer} can be associated with a given MIME type in an {@link UWSService} instance ! * Thus, if you add a {@link UWSSerializer} to a UWS, and this UWS has already a serializer for the same MIME type, * it will be replaced by the added one.
* *Note: A XML document can be linked to a XSLT style-sheet. By using the method {@link XMLSerializer#setXSLTPath(String)}
* you can define the path/URL of the XLST to link to each UWS resource.
* Since the {@link XMLSerializer} is the default format for a UWS resource you can also use the function
* {@link UWSService#setXsltURL(String)} !
As for a job or a jobs list, a UWS is also a UWS resource. That's why it can also be serialized !
* *However in some cases it could more interesting to substitute this resource by a home page of the whole UWS by using the function: * {@link #setHomePage(String)} or {@link #setHomePage(URL, boolean)}. *
* *Note: To go back to the UWS serialization (that is to say to abort a call to {@link #setHomePage(String)}), * use the method {@link #setDefaultHomePage()} !
* * * @author Grégory Mantelet (CDS;ARI) * @version 4.1 (09/2014) */ public class UWSService implements UWS { /** Name of this UWS. */ protected String name = null; /** Description of this UWS. */ protected String description = null; /** List of all managed jobs lists. (it is a LinkedHashMap so that jobs lists are ordered by insertion) */ protected final MapBuilds a UWS (the base URI will be extracted at the first request directly from the request itself).
* ** 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 ! *
* *note: since no logger is provided, a default one is set automatically (see {@link DefaultUWSLog}).
* * @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 null. * * @see #UWSService(UWSFactory, UWSFileManager, UWSLog) */ public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager){ this(jobFactory, fileManager, (UWSLog)null); } /** *Builds a UWS (the base URI will be extracted at the first request directly from the request itself).
* ** 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 ! *
* * @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 null. */ public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger){ if (jobFactory == null) throw new NullPointerException("Missing UWS factory! Can not create a UWSService."); factory = jobFactory; if (fileManager == null) throw new NullPointerException("Missing UWS file manager! Can not create a UWSService."); this.fileManager = fileManager; this.logger = (logger == null) ? new DefaultUWSLog(this) : logger; errorWriter = new DefaultUWSErrorWriter(this.logger); // Initialize the list of jobs: mapJobLists = new LinkedHashMapBuilds a UWS with its base UWS URI.
* *note: since no logger is provided, a default one is set automatically (see {@link DefaultUWSLog}).
* * @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 null or empty. * * @see #UWSService(UWSFactory, UWSFileManager, UWSLog, String) */ public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final String baseURI) throws UWSException{ this(jobFactory, fileManager, null, baseURI); } /** * 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 null or empty. * * @see UWSUrl#UWSUrl(String) */ public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger, final String baseURI) throws UWSException{ this(jobFactory, fileManager, logger); // Extract the name of the UWS: try{ // Set the URL interpreter: urlInterpreter = new UWSUrl(baseURI); // ...and the name of this service: name = urlInterpreter.getUWSName(); // Log the successful initialization: logger.logUWS(LogLevel.INFO, this, "INIT", "UWS successfully initialized!", null); }catch(NullPointerException ex){ // Log the exception: // (since the first constructor has already been called successfully, the logger is now NOT NULL): logger.logUWS(LogLevel.FATAL, null, "INIT", "Invalid base UWS URI: " + baseURI + "! You should check the configuration of the service.", ex); // Throw a new UWSException with a more understandable message: throw new UWSException(UWSException.BAD_REQUEST, ex, "Invalid base UWS URI (" + baseURI + ")!"); } } /** *Builds a UWS with the given UWS URL interpreter.
* *note: since no logger is provided, a default one is set automatically (see {@link DefaultUWSLog}).
* * @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. * * @see #UWSService(UWSFactory, UWSFileManager, UWSLog, UWSUrl) */ public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSUrl urlInterpreter){ this(jobFactory, fileManager, null, urlInterpreter); } /** * 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. */ public UWSService(final UWSFactory jobFactory, final UWSFileManager fileManager, final UWSLog logger, final UWSUrl urlInterpreter){ this(jobFactory, fileManager, logger); setUrlInterpreter(urlInterpreter); if (this.urlInterpreter != null) logger.logUWS(LogLevel.INFO, this, "INIT", "UWS successfully initialized.", null); } /* ************** */ /* LOG MANAGEMENT */ /* ************** */ @Override public UWSLog getLogger(){ return logger; } /** * Gets the object used to write/format any error in a HttpServletResponse. * * @return The error writer/formatter. */ public final ServiceErrorWriter getErrorWriter(){ return errorWriter; } /** *Sets the object used to write/format any error in a HttpServletResponse.
* *Note: Nothing is done if the given writer is NULL !
* * @param errorWriter The new error writer/formatter. */ public final void setErrorWriter(ServiceErrorWriter errorWriter){ if (errorWriter != null) this.errorWriter = errorWriter; } /* ***************** */ /* GETTERS & SETTERS */ /* ***************** */ @Override public final String getName(){ return name; } /** * Sets the name of this UWS. * * @param name Its new name. */ public final void setName(String name){ this.name = name; } @Override public final String getDescription(){ return description; } /** * Sets the description of this UWS. * * @param description Its new description. */ public final void setDescription(String description){ this.description = description; } /** * Gets the base UWS URL. * * @return The base UWS URL. * * @see UWSUrl#getBaseURI() */ public final String getBaseURI(){ return (urlInterpreter == null) ? null : urlInterpreter.getBaseURI(); } @Override public final UWSUrl getUrlInterpreter(){ return urlInterpreter; } /** * Sets the UWS URL interpreter to use in this UWS. * * @param urlInterpreter Its new UWS URL interpreter (may be null. In this case, it will be created from the next request ; see {@link #executeRequest(HttpServletRequest, HttpServletResponse)}). */ public final void setUrlInterpreter(UWSUrl urlInterpreter){ this.urlInterpreter = urlInterpreter; if (name == null && urlInterpreter != null) name = urlInterpreter.getUWSName(); } /** *Gets the object which lets extracting the user ID from a HTTP request.
*note:If the returned user identifier is NULL, no job should have an owner.
* * @return The used UserIdentifier (MAY BE NULL). */ @Override public final UserIdentifier getUserIdentifier(){ return userIdentifier; } /** * Sets the object which lets extracting the use ID from a received request. * * @param identifier The UserIdentifier to use (may be null). */ public final void setUserIdentifier(UserIdentifier identifier){ userIdentifier = identifier; } @Override public final UWSFactory getFactory(){ return factory; } @Override public final UWSFileManager getFileManager(){ return fileManager; } @Override public final UWSBackupManager getBackupManager(){ return backupManager; } /** ** 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(String)}. *
* * @param backupManager Its new backup manager. */ public final void setBackupManager(final UWSBackupManager backupManager){ this.backupManager = backupManager; } /* ******************** */ /* 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(){ return homePage; } /** * Tells whether a redirection to the specified home page must be done or not. * * @return true if a redirection to the specified resource must be done * or false to copy it. */ public final boolean isHomePageRedirection(){ return homeRedirection; } /** * 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 null). * @param redirect true if a redirection to the specified resource must be done * or false to copy it. */ public final void setHomePage(URL homePageUrl, boolean redirect){ homePage = homePageUrl.toString(); homeRedirection = redirect; } /** *Sets the URI of the resource which must be used as home page of this UWS.
* A redirection will always be done on the specified resource. * * @param homePageURI The URL of the home page. */ public final void setHomePage(String homePageURI){ homePage = homePageURI; homeRedirection = true; } /** * 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 true if it is the default home page, false otherwise. */ public final boolean isDefaultHomePage(){ return homePage == null; } /** * Forgets the home page specified by using {@link UWSService#setHomePage(URL, boolean)} - if any - * and go back to the default home page (XML format). */ public final void setDefaultHomePage(){ homePage = null; homeRedirection = false; } /* ********************** */ /* SERIALIZERS MANAGEMENT */ /* ********************** */ /** * Gets the MIME type of the serializer to use by default. * * @return The MIME type of the default serializer. */ public final String getDefaultSerializer(){ return defaultSerializer; } /** * 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{ if (serializers.containsKey(mimeType)) defaultSerializer = mimeType; else throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing UWS serializer for the MIME types: " + mimeType + "! The default serializer won't be set."); } /** *Adds a serializer to this UWS
*WARNING: 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 !
* * @param serializer The serializer to add. * @return true if the serializer has been successfully added, false otherwise. */ public final boolean addSerializer(UWSSerializer serializer){ if (serializer != null){ serializers.put(serializer.getMimeType(), serializer); if (serializers.size() == 1) defaultSerializer = serializer.getMimeType(); return true; } return false; } /** * Tells whether this UWS has already a serializer with the given MIME type. * * @param mimeType A MIME type (only one). * * @return true if a serializer exists with the given MIME type, false otherwise. */ public final boolean hasSerializerFor(String mimeType){ return serializers.containsKey(mimeType); } /** * Gets the total number of serializers available in this UWS. * * @return The number of its serializers. */ public final int getNbSerializers(){ return serializers.size(); } /** * Gets an iterator of the list of all serializers available in this UWS. * * @return An iterator on its serializers. */ public final IteratorLets adding the given action to this UWS.
* *WARNING: 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, String, HttpServletRequest)}) lastly !
* * @param action The UWS action to add. * * @return true if the given action has been successfully added, false otherwise. */ public final boolean addUWSAction(UWSAction action){ if (!uwsActions.contains(action)) return uwsActions.add(action); else return false; } /** *Lets inserting the given action at the given position in the actions list of this UWS.
* * @param indAction The index where the given action must be inserted. * @param action The action to add. * * @return true if the given action has been successfully added, false otherwise. * * @throws ArrayIndexOutOfBoundsException If the given index is incorrect (index < 0 || index >= uwsActions.size()). */ public final boolean addUWSAction(int indAction, UWSAction action) throws ArrayIndexOutOfBoundsException{ if (!uwsActions.contains(action)){ uwsActions.add(indAction, action); return true; } return false; } /** * Replaces the specified action by the given action. * * @param indAction Index of the action to replace. * @param action The replacer. * * @return true if the replacement has been a success, false otherwise. * * @throws ArrayIndexOutOfBoundsException If the index is incorrect (index < 0 || index >= uwsActions.size()). */ public final boolean setUWSAction(int indAction, UWSAction action) throws ArrayIndexOutOfBoundsException{ if (!uwsActions.contains(action)){ uwsActions.set(indAction, action); return true; } return false; } /** * Replaces the action which has the same name that the given action. * * @param action The replacer. * * @return The replaced action * or null if the given action is null * or if there is no action with the same name (in this case, the given action is not added). */ public final UWSAction replaceUWSAction(UWSAction action){ if (action == null) return null; else{ for(int i = 0; i < uwsActions.size(); i++){ if (uwsActions.get(i).equals(action)) return uwsActions.set(i, action); } return null; } } /** * Gets the number of actions this UWS has. * * @return The number of its actions. */ public final int getNbUWSActions(){ return uwsActions.size(); } /** * 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 null if there is no corresponding action. */ public final UWSAction getUWSAction(String actionName){ for(int i = 0; i < uwsActions.size(); i++){ if (uwsActions.get(i).getName().equals(actionName)) return uwsActions.get(i); } return null; } /** * Gets all actions of this UWS. * * @return An iterator on its actions. */ public final IteratorGenerate a unique ID for the given request.
* *By default, a timestamp is returned.
* * @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){ String id; do{ id = System.currentTimeMillis() + ""; }while(lastRequestID != null && lastRequestID.startsWith(id)); lastRequestID = id; return id; } /** *Executes the given request according to the IVOA Proposed Recommendation of 2010-02-10. * The result is returned in the given response.
* *Here is the followed algorithm:
*Sends a redirection (with the HTTP status code 303) to the given URL/URI into the given response.
* * @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. */ public void redirect(String url, HttpServletRequest request, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException, UWSException{ response.setStatus(HttpServletResponse.SC_SEE_OTHER); response.setContentType(request.getContentType()); response.setHeader("Location", url); response.flushBuffer(); } /** ** Fills the response with the given error. The HTTP status code is set in function of the error code of the given UWSException. * 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. *
* * @param error The error to send/display. * @param request The request which has caused the given error (not used by default). * @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 {@link ServiceErrorWriter#writeError(Throwable, HttpServletResponse, HttpServletRequest, JobOwner, String)} */ public final void sendError(UWSException error, HttpServletRequest request, String reqID, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException, UWSException{ if (error.getHttpErrorCode() == UWSException.SEE_OTHER){ // Log the redirection, if any: logger.logHttp(LogLevel.INFO, response, reqID, user, "HTTP " + UWSException.SEE_OTHER + " [Redirection toward " + error.getMessage() + "] - Action \"" + uwsAction + "\" successfully executed.", null); // Apply the redirection: redirect(error.getMessage(), request, user, uwsAction, response); }else sendError((Exception)error, request, reqID, user, uwsAction, response); } /** ** Fills the response with the given error. * The stack trace of the error is printed on the standard output and then the function * {@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. *
* * * @param error The error to send/display. * @param request The request which has caused the given error (not used by default). * @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 {@link ServiceErrorWriter#writeError(Throwable, HttpServletResponse, HttpServletRequest, String, JobOwner, String)} */ public final void sendError(Exception error, HttpServletRequest request, String reqID, JobOwner user, String uwsAction, HttpServletResponse response) throws IOException{ // Write the error in the response and return the appropriate HTTP status code: errorWriter.writeError(error, response, request, reqID, user, uwsAction); // Log the error: logger.logHttp(LogLevel.ERROR, response, reqID, user, "HTTP " + response.getStatus() + " - Can not complete the UWS action \"" + uwsAction + "\", because: " + error.getMessage(), error); } }