diff --git a/src/org/json/Json4Uws.java b/src/org/json/Json4Uws.java index 9e84863d5585d672a5166d1422d4473b2c3287a2..0dea60dcd9ee9eb63438ef6528af2ae56b29c89d 100644 --- a/src/org/json/Json4Uws.java +++ b/src/org/json/Json4Uws.java @@ -16,17 +16,19 @@ package org.json; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012,2014 - 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.util.Iterator; import uws.ISO8601Format; +import uws.UWSException; import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; +import uws.job.jobInfo.JobInfo; import uws.job.user.JobOwner; import uws.service.UWS; import uws.service.UWSUrl; @@ -35,7 +37,7 @@ import uws.service.UWSUrl; * Useful conversion functions from UWS to JSON. * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (12/2014) + * @version 4.2 (06/2017) */ public final class Json4Uws { @@ -139,11 +141,46 @@ public final class Json4Uws { json.put(UWSJob.PARAM_PARAMETERS, getJobParamsJson(job)); json.put(UWSJob.PARAM_RESULTS, getJobResultsJson(job)); json.put(UWSJob.PARAM_ERROR_SUMMARY, getJson(job.getErrorSummary())); + if (job.getJobInfo() != null) + json.put(UWSJob.PARAM_JOB_INFO, getJobInfoJson(job)); } } return json; } + /** + * Gets the JSON representation of the jobInfo of the given job. + * + * <p><b>Important note:</b> + * This function transforms the XML returned by + * {@link JobInfo#getXML(String)} into a JSON object + * (see {@link XML#toJSONObject(String)}). + * </p> + * + * @param job The job whose the jobInfo must be represented + * in JSON. + * + * @return The JSON representation of its jobInfo. + * + * @throws JSONException If there is an error while building the JSON + * object. + * + * @see JobInfo#getXML(String) + * @see XML#toJSONObject(String) + * + * @since 4.2 + */ + public final static JSONObject getJobInfoJson(final UWSJob job) throws JSONException{ + if (job.getJobInfo() != null){ + try{ + return XML.toJSONObject(job.getJobInfo().getXML(null)); + }catch(UWSException ue){ + throw new JSONException(ue); + } + }else + return null; + } + /** * Gets the JSON representation of the parameters of the given job. * @param job The job whose the parameters must be represented in JSON. diff --git a/src/tap/TAPRequestParser.java b/src/tap/TAPRequestParser.java index 91c24c6099e227ee1212fbdaccf8738ac06359ac..ba14f935b2428ad159c9f157d2e42fd81e34ffb4 100644 --- a/src/tap/TAPRequestParser.java +++ b/src/tap/TAPRequestParser.java @@ -16,7 +16,7 @@ package tap; * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2014 - Astronomisches Rechen Institut (ARI) + * Copyright 2014-2017 - Astronomisches Rechen Institut (ARI) */ import java.io.IOException; @@ -30,55 +30,58 @@ import uws.UWSToolBox; import uws.service.file.UWSFileManager; import uws.service.request.FormEncodedParser; import uws.service.request.MultipartParser; -import uws.service.request.NoEncodingParser; import uws.service.request.RequestParser; import uws.service.request.UploadFile; /** - * <p>This parser adapts the request parser to use in function of the request content-type:</p> + * This parser adapts the request parser to use in function of the request + * content-type: + * * <ul> * <li><b>application/x-www-form-urlencoded</b>: {@link FormEncodedParser}</li> * <li><b>multipart/form-data</b>: {@link MultipartParser}</li> - * <li><b>other</b>: {@link NoEncodingParser} (the whole request body will be stored as one single parameter)</li> + * <li><b>other</b>: no parameter is returned</li> * </ul> * * <p> - * The request body size is limited for the multipart AND the no-encoding parsers. If you want to change this limit, - * you MUST do it for each of these parsers, setting the following static attributes: resp. {@link MultipartParser#SIZE_LIMIT} - * and {@link NoEncodingParser#SIZE_LIMIT}. - * </p> + * The request body size is limited for the multipart. If you want to change + * this limit, you MUST do it for each of these parsers, setting the following + * static attributes: {@link MultipartParser#SIZE_LIMIT}. + * </p> * * <p><i>Note: - * If you want to change the support other request parsing, you will have to write your own {@link RequestParser} implementation. + * If you want to change the support other request parsing, you will have to + * write your own {@link RequestParser} implementation. * </i></p> * * @author Grégory Mantelet (ARI) - * @version 2.0 (12/2014) + * @version 2.1 (06/2017) * @since 2.0 */ public class TAPRequestParser implements RequestParser { /** File manager to use to create {@link UploadFile} instances. - * It is required by this new object to execute open, move and delete operations whenever it could be asked. */ + * It is required by this new object to execute open, move and delete + * operations whenever it could be asked. */ private final UWSFileManager fileManager; - /** {@link RequestParser} to use when a application/x-www-form-urlencoded request must be parsed. This attribute is set by {@link #parse(HttpServletRequest)} - * only when needed, by calling the function {@link #getFormParser()}. */ + /** {@link RequestParser} to use when a application/x-www-form-urlencoded + * request must be parsed. This attribute is set by + * {@link #parse(HttpServletRequest)} only when needed, by calling the + * function {@link #getFormParser()}. */ private RequestParser formParser = null; - /** {@link RequestParser} to use when a multipart/form-data request must be parsed. This attribute is set by {@link #parse(HttpServletRequest)} + /** {@link RequestParser} to use when a multipart/form-data request must be + * parsed. This attribute is set by {@link #parse(HttpServletRequest)} * only when needed, by calling the function {@link #getMultipartParser()}. */ private RequestParser multipartParser = null; - /** {@link RequestParser} to use when none of the other parsers can be used ; it will then transform the whole request body in a parameter called "JDL" - * (Job Description Language). This attribute is set by {@link #parse(HttpServletRequest)} only when needed, by calling the function - * {@link #getNoEncodingParser()}. */ - private RequestParser noEncodingParser = null; - /** - * Build a {@link RequestParser} able to choose the most appropriate {@link RequestParser} in function of the request content-type. + * Build a {@link RequestParser} able to choose the most appropriate + * {@link RequestParser} in function of the request content-type. * - * @param fileManager The file manager to use in order to store any eventual upload. <b>MUST NOT be NULL</b> + * @param fileManager The file manager to use in order to store any + * eventual upload. <b>MUST NOT be NULL</b> */ public TAPRequestParser(final UWSFileManager fileManager){ if (fileManager == null) @@ -103,7 +106,7 @@ public class TAPRequestParser implements RequestParser { else if (MultipartParser.isMultipartContent(req)) params = getMultipartParser().parse(req); else - params = getNoEncodingParser().parse(req); + params = new HashMap<String,Object>(0); // Only for POST requests, the parameters specified in the URL must be added: if (method.equals("post")) @@ -115,10 +118,12 @@ public class TAPRequestParser implements RequestParser { } /** - * Get the {@link RequestParser} to use for application/x-www-form-urlencoded HTTP requests. + * Get the {@link RequestParser} to use for + * application/x-www-form-urlencoded HTTP requests. * This parser may be created if not already done. * - * @return The {@link RequestParser} to use for application/x-www-form-urlencoded requests. <i>Never NULL</i> + * @return The {@link RequestParser} to use for + * application/x-www-form-urlencoded requests. <i>Never NULL</i> */ private synchronized final RequestParser getFormParser(){ return (formParser != null) ? formParser : (formParser = new FormEncodedParser(){ @@ -142,10 +147,12 @@ public class TAPRequestParser implements RequestParser { } /** - * Get the {@link RequestParser} to use for multipart/form-data HTTP requests. + * Get the {@link RequestParser} to use for multipart/form-data HTTP + * requests. * This parser may be created if not already done. * - * @return The {@link RequestParser} to use for multipart/form-data requests. <i>Never NULL</i> + * @return The {@link RequestParser} to use for multipart/form-data + * requests. <i>Never NULL</i> */ private synchronized final RequestParser getMultipartParser(){ return (multipartParser != null) ? multipartParser : (multipartParser = new MultipartParser(fileManager){ @@ -176,25 +183,17 @@ public class TAPRequestParser implements RequestParser { } /** - * Get the {@link RequestParser} to use for HTTP requests whose the content type is neither application/x-www-form-urlencoded nor multipart/form-data. - * This parser may be created if not already done. - * - * @return The {@link RequestParser} to use for requests whose the content-type is not supported. <i>Never NULL</i> - */ - private synchronized final RequestParser getNoEncodingParser(){ - return (noEncodingParser == null) ? (noEncodingParser = new NoEncodingParser(fileManager)) : noEncodingParser; - } - - /** - * Create a new array in which the given String is appended at the end of the given array. + * Create a new array in which the given String is appended at the end of + * the given array. * * @param value String to append in the array. * @param oldValue The array after which the given String must be appended. * - * @return The new array containing the values of the array and then the given String. + * @return The new array containing the values of the array and then the + * given String. */ private final static String[] append(final String value, final String[] oldValue){ - // Create the corresponding array of Strings: + // Create the corresponding array of Strings: // ...if the array already exists, extend it: String[] newValue; if (oldValue != null){ diff --git a/src/uws/config/ConfigurableUWSFactory.java b/src/uws/config/ConfigurableUWSFactory.java index 078149805262348f62baa6fa0dfe79b53b1a4d9d..19bbb156e3654234aff23b2f61f542b9a1cc1f74 100644 --- a/src/uws/config/ConfigurableUWSFactory.java +++ b/src/uws/config/ConfigurableUWSFactory.java @@ -16,7 +16,7 @@ package uws.config; * 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 2016 - Astronomisches Rechen Institut (ARI) + * Copyright 2016-2017 - Astronomisches Rechen Institut (ARI) */ import static uws.config.UWSConfiguration.KEY_REGEXP_MAX_DESTRUCTION_INTERVAL; @@ -49,6 +49,7 @@ import uws.job.ErrorSummary; import uws.job.JobThread; import uws.job.Result; import uws.job.UWSJob; +import uws.job.jobInfo.JobInfo; import uws.job.parameters.DestructionTimeController; import uws.job.parameters.DurationParamController; import uws.job.parameters.ExecutionDurationController; @@ -68,7 +69,7 @@ import uws.service.request.UWSRequestParser; * Concrete implementation of a {@link UWSFactory} which is parameterized by a UWS configuration file. * * @author Grégory Mantelet (ARI) - * @version 4.2 (09/2016) + * @version 4.2 (06/2017) * @since 4.2 */ public class ConfigurableUWSFactory implements UWSFactory { @@ -620,7 +621,14 @@ public class ConfigurableUWSFactory implements UWSFactory { requestID = request.getAttribute(UWS.REQ_ATTRIBUTE_ID).toString(); // Create the job: - return new UWSJob(user, createUWSParameters(request), requestID); + UWSJob newJob = new UWSJob(user, createUWSParameters(request), requestID); + + // Set the XML job description if any: + Object jobDesc = request.getAttribute(UWS.REQ_ATTRIBUTE_JOB_DESCRIPTION); + if (jobDesc != null && jobDesc instanceof JobInfo) + newJob.setJobInfo((JobInfo)jobDesc); + + return newJob; } @Override diff --git a/src/uws/job/UWSJob.java b/src/uws/job/UWSJob.java index 598a5c222fd2b5e4481439dd31b07651da6ae1fa..885bb40e91d343b7b94a8a789dd701af61939dbb 100644 --- a/src/uws/job/UWSJob.java +++ b/src/uws/job/UWSJob.java @@ -16,7 +16,7 @@ package uws.job; * 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-2016 - 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) */ @@ -37,6 +37,7 @@ import uws.ISO8601Format; import uws.UWSException; import uws.UWSExceptionFactory; import uws.UWSToolBox; +import uws.job.jobInfo.JobInfo; import uws.job.manager.ExecutionManager; import uws.job.parameters.UWSParameters; import uws.job.serializer.UWSSerializer; @@ -114,7 +115,7 @@ import uws.service.request.UploadFile; * </ul> * * @author Grégory Mantelet (CDS;ARI) - * @version 4.2 (01/2016) + * @version 4.2 (06/2017) */ public class UWSJob extends SerializableUWSObject { private static final long serialVersionUID = 1L; @@ -170,6 +171,10 @@ public class UWSJob extends SerializableUWSObject { /** Name of the parameter <i>results</i>. */ public static final String PARAM_RESULTS = "results"; + /** Name of the parameter <i>jobInfo</i>. + * @since 4.2 */ + public static final String PARAM_JOB_INFO = "jobInfo"; + /** Default value of {@link #owner} if no ID are given at the job creation. */ public final static String ANONYMOUS_OWNER = "anonymous"; @@ -249,6 +254,10 @@ public class UWSJob extends SerializableUWSObject { /** List of all input parameters (UWS standard and non-standard parameters). */ protected final UWSParameters inputParams; + /** Additional description of this job. + * @since 4.2 */ + protected JobInfo jobInfo = null; + /** The thread to start for executing the job. */ protected transient JobThread thread = null; @@ -420,7 +429,7 @@ public class UWSJob extends SerializableUWSObject { try{ setPhase(p, true); }catch(UWSException ue){ - // Can never append because the "force" parameter is true! + // Can never append because the "force" parameter is true! } } @@ -1015,7 +1024,7 @@ public class UWSJob extends SerializableUWSObject { * @see #applyPhaseParam(JobOwner) */ public boolean addOrUpdateParameters(UWSParameters params, final JobOwner user) throws UWSException{ - // The job can be modified ONLY IF in PENDING phase: + // The job can be modified ONLY IF in PENDING phase: if (!phase.isJobUpdatable()) throw new UWSException(UWSException.FORBIDDEN, "Forbidden parameters modification: the job is not any more in the PENDING phase!"); @@ -1143,6 +1152,54 @@ public class UWSJob extends SerializableUWSObject { } } + /** + * Get the additional information about this job. + * + * @return Additional info. about this job, + * or NULL if there is none. + * + * @since 4.2 + */ + public final JobInfo getJobInfo(){ + return jobInfo; + } + + /** + * Set the additional information about this job. + * + * <p><i>Note: + * By default, this function replaces the current {@link JobInfo} + * of this job by the given one (even if NULL). This behavior + * can be changed by overwriting this function and by returning the + * extended {@link UWSJob} in the used {@link UWSFactory}. + * </i></p> + * + * <p><b>Important note:</b> + * When attributing a {@link JobInfo} to a {@link UWSJob}, you + * may have to call {@link JobInfo#setJob(UWSJob)} on the former + * and the new jobInfo (see the default implementation for an example) + * for some implementations of {@link JobInfo}. + * </p> + * + * @param newJobInfo The new additional info. about this job. + * <i>NULL is allowed and should be used to remove a + * JobInfo from a job.</i> + * + * @since 4.2 + */ + public void setJobInfo(final JobInfo newJobInfo){ + // Cut the link between the former jobInfo and this job: + if (this.jobInfo != null) + this.jobInfo.setJob(null); + + // Establish a link between the new jobInfo and this job: + if (newJobInfo != null) + newJobInfo.setJob(this); + + // Replace the former jobInfo by the given one: + this.jobInfo = newJobInfo; + } + /** * Gets the execution manager of this job, if any. * @@ -1288,7 +1345,7 @@ public class UWSJob extends SerializableUWSObject { } /** - * Stop/Cancel this job when its maximum execution duration has been reached. + * Stop/Cancel this job when its maximum execution duration has been reached. * * @author Grégory Mantelet (CDS;ARI) * @version 4.1 (09/2014) @@ -1458,7 +1515,7 @@ public class UWSJob extends SerializableUWSObject { * <p>Stops the job if running, removes the job from the execution manager, stops the timer for the execution duration * and may clear all files or any other resources associated to this job.</p> * - * <p><i>By default the job is aborted, the {@link UWSJob#thread} attribute is set to null, the timers are stopped and uploaded files, results and the error summary are deleted.</i></p> + * <p><i>By default the job is aborted, the {@link UWSJob#thread} attribute is set to null, the timers are stopped and uploaded files, results and the error summary are deleted and the jobInfo is destroyed.</i></p> */ public void clearResources(){ // If still running, abort/stop the job: @@ -1507,6 +1564,15 @@ public class UWSJob extends SerializableUWSObject { } } + // Destroy the additional job info.: + if (jobInfo != null){ + try{ + jobInfo.destroy(); + }catch(UWSException ue){ + getLogger().logJob(LogLevel.ERROR, this, "CLEAR_RESOURCES", "Impossible to destroy the additional information about the job \"" + jobId + "\"", ue); + } + } + getLogger().logJob(LogLevel.INFO, this, "CLEAR_RESOURCES", "Resources associated with the job \"" + getJobId() + "\" have been successfully freed.", null); } @@ -1677,7 +1743,7 @@ public class UWSJob extends SerializableUWSObject { @Override public String toString(){ - return "JOB {jobId: " + jobId + "; phase: " + phase + "; runId: " + getRunId() + "; ownerId: " + owner + "; executionDuration: " + getExecutionDuration() + "; destructionTime: " + getDestructionTime() + "; quote: " + quote + "; NbResults: " + results.size() + "; " + ((errorSummary != null) ? errorSummary.toString() : "No error") + " }"; + return "JOB {jobId: " + jobId + "; phase: " + phase + "; runId: " + getRunId() + "; ownerId: " + owner + "; executionDuration: " + getExecutionDuration() + "; destructionTime: " + getDestructionTime() + "; quote: " + quote + "; NbResults: " + results.size() + "; " + ((errorSummary != null) ? errorSummary.toString() : "No error") + " ; HasJobInfo: \"" + ((jobInfo != null) ? "yes" : "no") + "\" }"; } @Override diff --git a/src/uws/job/jobInfo/JobInfo.java b/src/uws/job/jobInfo/JobInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..7daea60a6c37a698d125c41de2a914045496dd41 --- /dev/null +++ b/src/uws/job/jobInfo/JobInfo.java @@ -0,0 +1,207 @@ +package uws.job.jobInfo; + +/* + * 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.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +import javax.servlet.http.HttpServletResponse; + +import uws.UWSException; +import uws.job.UWSJob; +import uws.job.serializer.XMLSerializer; + +/** + * API wrapping an object which provides more information about a job. + * + * <p> + * It can be a simple information (e.g. job progress ; + * see {@link SingleValueJobInfo}) or a whole job description instead of + * parameters as described in REC-UWS-1.0, + * "1.3. Job description language, service contracts and universality" + * (e.g. an XML document ; see {@link XMLJobInfo}). + * </p> + * + * <p><b>Representation</b></p> + * <p> + * As requested by REC-UWS-1.0, the function {@link #getXML(String)} + * must return an XML representation of this jobInfo, but that does not + * mean that the additional job information have to be in XML ; they can be + * an XML, a .txt, an image, ... The function {@link #getXML(String)} just + * needs to return a representation of this jobInfo: + * either the jobInfo content itself or a link to access it into details. + * </p> + * <p> + * The function {@link #write(HttpServletResponse)} is only used when + * ONLY the content of a job's jobInfo is requested: + * with the URL <code>{uws-root}/{job-list}/{job-id}/jobInfo</code>. + * It allows to return the real content of this jobInfo (if not already + * the XML returned by {@link #getXML(String)}). + * </p> + * + * <p><b>Resource management</b></p> + * <p> + * In case the jobInfo is associated with other resources (e.g. memory, file, + * ...), the function {@link #destroy()} must be able to discard them. + * This function is always called at job destruction. + * </p> + * + * <p><b>Backup</b></p> + * <p> + * The implementation of a {@link JobInfo} being free, the only viable way to + * backup a such object is by Java Class Serialization (see + * {@link Serializable}, {@link ObjectOutputStream} and + * {@link ObjectInputStream}). A default serialization is already implemented, + * but it can be customized by overriding the following functions: + * </p> + * <ul> + * <li><code>private void writeObject(java.io.ObjectOutputStream out) + * throws IOException</code></li> + * <li><code>private void readObject(java.io.ObjectInputStream in) + * throws IOException, ClassNotFoundException;</code></li> + * <li><code>private void readObjectNoData() + * throws ObjectStreamException;</code></li> + * </ul> + * + * <p><i>See the Javadoc of {@link Serializable} for more details.</i></p> + * + * <p><b>Link with {@link UWSJob}</b></p> + * + * <p> + * Once a {@link JobInfo} is attached to a job (thanks to + * {@link UWSJob#setJobInfo(JobInfo)}), the function {@link #setJob(UWSJob)} is + * called. In some implementation, no action is needed (see + * {@link SingleValueJobInfo}), but in some others it may be required to either + * keep a link with the parent job or to execute some special action. + * </p> + * + * <p><b>Warning:</b> + * Since a {@link JobInfo} must be {@link Serializable} it is recommended to + * flag complex objects like {@link UWSJob} as <i>transient</i> as much as + * possible in order to make the backup and restore processes lighter. + * Some objects like the parent job are naturally restored when + * {@link #setJob(UWSJob)} is called. See {@link XMLJobInfo} for a concrete + * example. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 4.2 (06/2017) + * @since 4.2 + */ +public interface JobInfo extends Serializable { + + /** + * Get the XML representation of this {@link JobInfo}. + * + * <p><i>Note 1: + * This function does not force the jobInfo to be in + * XML but asks for a piece of XML document to append + * to the XML representation of a job and representing + * this jobInfo. It may be a full serialization of it or + * merely a link (see <a href="https://www.w3.org/TR/xlink11/">Xlink</a>) + * toward a complete document (XML or not). + * </i></p> + * + * <p><i>Note 2: + * The returned piece of XML can refer to the following + * XML schemas: + * </i></p> + * <ul><i> + * <li>"http://www.ivoa.net/xml/UWS/v1.0" (no prefix),</li> + * <li>"http://www.w3.org/1999/xlink" (prefix: xlink),</li> + * <li>"http://www.w3.org/2001/XMLSchema" (prefix: xs),</li> + * <li>"http://www.w3.org/2001/XMLSchema-instance" (prefix: xsi).</li> + * </i></ul> + * <p><i> + * If more namespaces are needed they should be specified directly + * at the root of the XML returned by this function (if possible + * with a valid <code>xsi:schemaLocation</code>). An alternative + * would be to extend {@link XMLSerializer} in order to append + * the needed namespaces to the root XML node of any formatted XML + * documents. + * </i></p> + * + * @param newLinePrefix Characters (generally white-spaces) that should + * prefix all new line of the returned piece of + * XML. New line characters should also be + * included in this string ; if not, the + * returned XML should be on a single line. + * <i>This parameter may be NULL.</i> + * + * @return XML representation of this jobInfo. + * + * @throws UWSException If any error occurs while building the XML + * representation of this jobInfo. + */ + public String getXML(final String newLinePrefix) throws UWSException; + + /** + * Write the content of this jobInfo as a complete HTTP response + * when the URL <code>{uws-root}/{job-list}/{job-id}/jobInfo</code> is + * requested. + * + * <p><b>Important:</b> + * At least the Content-Type, the Content-Length and Character-Encoding + * should be set in addition of the response content. + * </p> + * + * <p><i>Note: + * If formatted into XML, the root node of the returned document + * may be the UWS node "jobInfo" or not, depending on your + * desired implementation. Since the UWS standard does not specify + * any way to retrieve individually a jobInfo, this part is left + * here totally free to the developer will. + * </i></p> + * + * @param response HTTP response in which the jobInfo content must be + * written. + * + * @throws IOException If there is any error while writing the jobInfo + * content. + * @throws UWSException If there is any error while formating the jobInfo + * content. + */ + public void write(final HttpServletResponse response) throws IOException, UWSException; + + /** + * Notify this {@link JobInfo} that it is now owned by the given job. + * + * @param myJob The new owner of this {@link JobInfo}. + * <i>This parameter may be NULL.</i> + */ + public void setJob(final UWSJob myJob); + + /** + * Free/Discard any resource associated with this {@link JobInfo}. + * + * <p><i>Note:</i> + * This function should be called only at job destruction. + * It particularly aims to delete any file containing the full + * content of this JobInfo, but it should also be used for any + * other kind of associated resource. + * </p> + * + * @throws UWSException If all associated resources can not be freed. + */ + public void destroy() throws UWSException; + +} diff --git a/src/uws/job/jobInfo/SingleValueJobInfo.java b/src/uws/job/jobInfo/SingleValueJobInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..f08be0b75eb25f4e121d62cb1a50835a985721d6 --- /dev/null +++ b/src/uws/job/jobInfo/SingleValueJobInfo.java @@ -0,0 +1,195 @@ +package uws.job.jobInfo; + +/* + * 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.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServletResponse; + +import uws.UWSException; +import uws.job.UWSJob; +import uws.job.serializer.XMLSerializer; + +/** + * Very simple implementation of {@link JobInfo}. It aims to represent a + * key-value pair. + * + * <p> + * Both functions {@link #getXML(String)} and + * {@link #write(HttpServletResponse)} will return the following XML document: + * </p> + * + * <pre><KEY>VALUE</KEY></pre> + * + * <p>, where:</p> + * <ul> + * <li><i><code>KEY</code></i> can be get with {@link #getName()} and can be + * set <b>only</b> at creation</li> + * <li><i><code>VALUE</code></i> can be get with {@link #getValue()} and set + * with {@link #setValue(String)}.</li> + * </ul> + * + * @author Grégory Mantelet (ARI) + * @version 4.2 (06/2017) + * @since 4.2 + */ +public class SingleValueJobInfo implements JobInfo { + private static final long serialVersionUID = 1L; + + /** Name of the value stored inside this {@link JobInfo}. + * + * <p><i><b>Warning:</b> By default, this name is not supposed to be + * changed after initialization of this class. That's why only a public + * getter function is provided.</i></p> */ + protected String name = null; + + /** Value stored inside this {@link JobInfo}. */ + protected String value = null; + + /** XML representation of this {@link JobInfo}, returned by + * {@link #getXML(String)} and {@link #write(HttpServletResponse)}. + * + * <p><i>Note: + * It has to be updated each time the {@link #value} is changed. So by + * default, it is rebuilt by {@link #setValue(String)}. + * </i></p> */ + protected String xmlRepresentation = null; + + /** + * Build a {@link JobInfo} representing a single value having the given + * name. + * + * <p><i>Note 1: + * The name can not be changed after creation. + * </i></p> + * + * <p><i>Note 2: + * With this constructor, the represented value is NULL. To set a value, + * you have to use the function {@link #setValue(String)}. An alternative + * would be to use the constructor + * {@link #SingleValueJobInfo(String, String)} so that setting immediately + * the name and value. + * </i></p> + * + * @param name Name of the value to represent. + * + * @throws NullPointerException If the given name is NULL or an empty + * string. + * @throws IllegalArgumentException If the given name is not a valid XML + * node name according to the W3C (see + * {@link XMLSerializer#isValidXMLNodeName(String)} + * for more details). + */ + public SingleValueJobInfo(final String name) throws NullPointerException, IllegalArgumentException{ + this(name, null); + } + + /** + * Build a {@link JobInfo} representing a single value having the given + * name and initial value. + * + * <p><i>Note 1: + * The name can not be changed after creation. + * </i></p> + * + * <p><i>Note 2: + * The value can change after object creation with the function + * {@link #setValue(String)}. + * </i></p> + * + * @param name Name of the value to represent. <i>Can not be NULL or an + * empty string, and must be a valid XML node name.</i> + * @param value Value to represent. <i>May be NULL.</i> + * + * @throws NullPointerException If the given name is NULL or an empty + * string. + * @throws IllegalArgumentException If the given name is not a valid XML + * node name according to the W3C (see + * {@link XMLSerializer#isValidXMLNodeName(String)} + * for more details). + */ + public SingleValueJobInfo(final String name, final String value) throws NullPointerException, IllegalArgumentException{ + if (name == null || name.trim().length() == 0) + throw new NullPointerException("Missing SingleValueJobInfo name!"); + else if (!XMLSerializer.isValidXMLNodeName(name)) + throw new IllegalArgumentException("Invalid XML node name: \"" + name + "\"! You should choose a different name for your SingleValueJobInfo."); + + this.name = name; + setValue(value); + } + + /** + * Get the name of the represented value. + * + * @return Value name. <i>Can NEVER be NULL.</i> + */ + public String getName(){ + return name; + } + + /** + * Get the represented value. + * + * @return The represented value. <i>Can be NULL.</i> + */ + public String getValue(){ + return value; + } + + /** + * Set the value represented by this {@link JobInfo}. + * + * @param value The new value to represent. <i>Can be NULL.</i> + */ + public void setValue(final String value){ + this.value = value; + + xmlRepresentation = "<" + name + ">" + XMLSerializer.escapeXMLData(this.value) + "</" + name + ">"; + } + + @Override + public String getXML(final String newLinePrefix){ + return xmlRepresentation; + } + + @Override + public void write(HttpServletResponse response) throws IOException, UWSException{ + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/xml"); + response.setContentLength(xmlRepresentation.getBytes("UTF-8").length); + + PrintWriter writer = response.getWriter(); + writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); + writer.println(xmlRepresentation); + writer.flush(); + } + + @Override + public void setJob(final UWSJob myJob){ + // Nothing to do! + } + + @Override + public void destroy() throws UWSException{ + // Nothing to do! + } + +} diff --git a/src/uws/job/jobInfo/XMLJobInfo.java b/src/uws/job/jobInfo/XMLJobInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..39ce547e60f154d8297990e386facec14f426744 --- /dev/null +++ b/src/uws/job/jobInfo/XMLJobInfo.java @@ -0,0 +1,390 @@ +package uws.job.jobInfo; + +/* + * 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.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.Serializable; + +import javax.servlet.http.HttpServletResponse; + +import uws.UWSException; +import uws.UWSToolBox; +import uws.job.UWSJob; +import uws.job.serializer.XMLSerializer; +import uws.service.UWS; +import uws.service.file.UWSFileManager; +import uws.service.log.UWSLog.LogLevel; +import uws.service.request.UploadFile; + +/** + * A full XML document attached to a {@link UWSJob job}. + * + * <p><b>XML representation</b></p> + * + * <p> + * The document stored inside this {@link JobInfo} is considered formatted + * in XML. So the functions {@link #getXML(String)} and + * {@link #write(HttpServletResponse)} will return them as such. + * </p> + * + * <p><i>Note 1: + * The represented document is supposed to be XML, but absolutely no + * verification is performed by {@link XMLJobInfo}. + * </i></p> + * + * <p><i>Note 2: + * {@link #getXML(String)} will skip the XML declaration + * (e.g. <code><?xml version="1.0" encoding="utf-8"?></code>) + * if any is provided. On the contrary, {@link #write(HttpServletResponse)} + * will write an exact copy of the stored XML document. + * Both functions can be overwritten if a different behavior is needed. + * </i></p> + * + * <p><i>Note 3: + * The stored XML document can refer to the following + * XML schemas: + * </i></p> + * <ul><i> + * <li>"http://www.ivoa.net/xml/UWS/v1.0" (no prefix),</li> + * <li>"http://www.w3.org/1999/xlink" (prefix: xlink),</li> + * <li>"http://www.w3.org/2001/XMLSchema" (prefix: xs),</li> + * <li>"http://www.w3.org/2001/XMLSchema-instance" (prefix: xsi).</li> + * </i></ul> + * <p><i> + * If more namespaces are needed they should be specified directly + * at the root of the stored XML document (if possible with a valid + * <code>xsi:schemaLocation</code>). An alternative would be to extend + * {@link XMLSerializer} in order to append the needed namespaces to the root + * XML node of any formatted XML documents. + * </i></p> + * + * <p><b>Internal representation and Creation</b></p> + * + * <p> + * This class proposes the two following constructors, each for a different + * internal representation: + * </p> + * <ul> + * <li><i>{@link #XMLJobInfo(String)} for an in-memory string.</i> The given + * string is supposed to contained the full XML document and will be + * stored as such in this class. This constructor should be used <b>only + * for small XML document</b>.</li> + * <li><i>{@link #XMLJobInfo(UploadFile)} for an XML file storage.</i> The + * given {@link UploadFile} is supposed to give access to the complete + * XML document.This constructor should be used <b>for large XML + * document.</b></li> + * </ul> + * + * <p><b>Modification</b></p> + * + * <p> + * By default, this implementation of {@link JobInfo} does not allow the + * modification of its XML document. If needed, this class should be + * extended with the adequate functions. + * </p> + * + * <p><b>Backup/Restoration</b></p> + * + * <p> + * An {@link UploadFile} can not be serialized using the Java Class + * Serialization mechanism because it does not implement the + * {@link Serializable} interface. Consequently, the given {@link UploadFile} + * will be marked as <i>transient</i> and will have to be rebuilt when needed + * after a restoration process. + * </p> + * + * <p>However, it can be rebuilt only if:</p> + * <ol> + * <li>an access to a {@link UWSFileManager} is + * possible.</li> + * <li>the location of the file is known.</li> + * </ol> + * + * <p> + * The first point (1) is fortunately possible through a {@link UWSJob} object. + * This object is known after attachment to a job thanks to the function + * {@link #setJob(UWSJob)}. So, a link toward the parent job should be kept ; + * also marked as <i>transient</i>: see the attribute {@link #job}. + * </p> + * + * <p>For the second point (2), the location of the file must be kept as a + * non-transient attribute (see {@link #location}) so that being backuped with the + * other non-transient attributes of this class. In order to backup this + * location up-to-date, the function + * {@link #writeObject(java.io.ObjectOutputStream)} updates {@link #location} + * before the normal Java Class Serialization. + * + * <p> + * So finally, the restoration of the {@link UploadFile} will be done by + * {@link #getXML(String)} and {@link #write(HttpServletResponse)} with the + * function {@link #restoreFile()}. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 4.2 (06/2017) + * @since 4.2 + */ +public class XMLJobInfo implements JobInfo { + private static final long serialVersionUID = 1L; + + /** XML file location. + * + * <p><b>Warning:</b> + * This field <b>ONLY</b> aims to contain the updated result of + * {@link UploadFile#getLocation() file.getLocation()}. + * </p> */ + protected String location = null; + + /** Link toward the XML file represented by this {@link JobInfo}. + * + * <p><b>Important:</b> + * This field must be used when a large XML document has to be represented + * by this {@link JobInfo}. + * </p> + * + * <p><i>Note: + * It can be set only by {@link #XMLJobInfo(UploadFile)}. + * If set, {@link #content} must be NULL. + * </i></p>*/ + protected transient UploadFile file = null; + + /** XML document represented by this {@link JobInfo}. + * + * <p><b>Important:</b> + * This field MUST be used <b>ONLY</b> for small XML document in order to + * keep enough memory free for the normal UWS service operations. + * </p> + * + * <p><i>Note: + * It can be set only by {@link #XMLJobInfo(String)}. + * If set, {@link #file} must be NULL. + * </i></p> */ + protected String content = null; + + /** Precise length (in bytes) of the represented XML document. */ + protected int length = -1; + + /** The job owning this {@link JobInfo}. + * + * <p><i>Note: + * This field is set only by {@link #setJob(UWSJob)} and is used + * only by {@link #restoreFile()} in order to rebuild {@link #file} + * after a UWS service restoration. + * </i></p> */ + protected transient UWSJob job = null; + + /** + * Build a {@link JobInfo} representing a <b>small</b> XML document. + * + * <p><b>Important:</b> + * This constructor should be used only for <b>small</b> XML document + * because the given string will be kept as such in memory. If the given + * string is too large, not enough memory will be available for normal + * UWS service operations.<br/><br/> + * <i>If you estimate the XML document is too big to stay in memory, you + * should save it in a file and use the constructor + * {@link #XMLJobInfo(UploadFile)}.</i> + * </p> + * + * @param smallXML The small XML document to represent. + * + * @throws NullPointerException If the given string is NULL or empty. + */ + public XMLJobInfo(final String smallXML) throws NullPointerException{ + if (smallXML == null || smallXML.trim().length() == 0) + throw new NullPointerException("Missing XML content!"); + + content = smallXML; + length = smallXML.getBytes().length; + file = null; + location = null; + } + + /** + * Build a {@link JobInfo} representing a <b>large</b> XML document stored + * inside a file. + * + * @param xmlFile Link toward the large XML document to represent. + * + * @throws NullPointerException If the given file is NULL or empty. + */ + public XMLJobInfo(final UploadFile xmlFile) throws NullPointerException{ + if (xmlFile == null || xmlFile.length <= 0) + throw new NullPointerException("Missing XML file!"); + + file = xmlFile; + location = file.getLocation(); + length = (int)file.length; + content = null; + } + + @Override + public String getXML(final String newLinePrefix) throws UWSException{ + // CASE: SMALL XML DOCUMENT: + if (content != null){ + if (content.trim().startsWith("<?")) + return content.substring(content.indexOf("?>") + 2); + else + return content; + + }// CASE: XML FILE + else{ + restoreFile(); + + StringBuffer xml = new StringBuffer(); + BufferedReader input = null; + try{ + // Open the XML file: + input = new BufferedReader(new InputStreamReader(file.open())); + String line; + // Read it line by line: + while((line = input.readLine()) != null){ + // Ignore the XML declarative lines: + if (line.trim().startsWith("<?")){ + line = line.substring(line.indexOf("?>") + 2); + if (line.trim().length() == 0) + continue; + } + // Append the line prefix (if any): + if (newLinePrefix != null && xml.length() > 0) + xml.append(newLinePrefix); + // Append the fetched line: + xml.append(line); + } + }catch(IOException ioe){ + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Impossible to get the XML representation of the JobInfo!"); + }finally{ + if (input != null){ + try{ + input.close(); + }catch(IOException ioe){} + } + } + return xml.toString(); + } + } + + @Override + public void write(final HttpServletResponse response) throws IOException, UWSException{ + // CASE: SMALL XML DOCUMENT: + if (content != null){ + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/xml"); + response.setContentLength(content.getBytes("UTF-8").length); + + PrintWriter writer = response.getWriter(); + writer.println(content); + writer.flush(); + } + + // CASE: XML FILE: + else{ + restoreFile(); + UWSToolBox.write(file.open(), "text/xml", file.length, response); + } + } + + @Override + public void setJob(final UWSJob myJob){ + job = myJob; + + if (job != null && file != null){ + try{ + file.move(job); + location = file.getLocation(); + }catch(IOException ioe){ + if (job.getLogger() != null) + job.getLogger().logUWS(LogLevel.ERROR, job, "SET_JOB_INFO", "Error when moving the XML JobInfo file closer to the job " + job.getJobId() + "! Current file location: " + file.getLocation(), ioe); + } + } + } + + @Override + public void destroy() throws UWSException{ + try{ + file.deleteFile(); + }catch(IOException ioe){ + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Error when deleting a JobInfo file!"); + } + } + + /** + * Serialize this {@link XMLJobInfo}. + * + * <p><i>Note:</i> + * This function will be called by the Java Class Serialization mechanism. + * See the Javadoc of {@link Serializable} for more details. + * </i></p> + * + * <p> + * This function just updates the XML file (if any) location before the + * normal Java Class Serialization of this object. + * </p> + * + * @param out The stream used to contained the serialization of this + * {@link XMLJobInfo}. + * + * @throws IOException If any error occurs while serializing this + * {@link XMLJobInfo} + */ + private void writeObject(java.io.ObjectOutputStream out) throws IOException{ + // Ensure the location is up-to-date before performing the backup of this jobInfo: + if (file != null) + location = file.getLocation(); + + // Apply the default Java serialization method: + out.defaultWriteObject(); + } + + /** + * Restore the link toward the XML file represented by this {@link JobInfo}. + * + * <p> + * This function has an effect only if {@link #file} is NULL but not + * {@link #location} ; indeed, such configuration can be encountered only + * if this {@link XMLJobInfo} has been de-serialized. + * </p> + * + * <p> + * Nothing can be done if the parent job is unknown. In other words, + * this {@link JobInfo} has to be attached to a job first + * (i.e. {@link #setJob(UWSJob)} has to be called first with a non-NULL + * parameter). If not, an exception will be thrown. + * </p> + * + * @throws UWSException If this {@link JobInfo} is not attached to a job. + */ + protected void restoreFile() throws UWSException{ + /* If the file is NULL, it means a UWS restore has just occurred. + * Because UploadFile is not Serializable, it was impossible to restore the file. + * To solve this problem, the location has been saved. + * So, the file can be and has to be restored. */ + if (file == null && location != null){ + if (job != null) + file = new UploadFile(UWS.REQ_ATTRIBUTE_JOB_DESCRIPTION, location, job.getFileManager()); + else + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, "Missing jobInfo's file: impossible to display its content! Cause: missing UWSJob parent."); + } + } + +} diff --git a/src/uws/job/serializer/XMLSerializer.java b/src/uws/job/serializer/XMLSerializer.java index 15fe433e8d66777a78a8544273d9e6e9e6172ef5..e59cdc3e88e768465e2432fce02ac695114e8f48 100644 --- a/src/uws/job/serializer/XMLSerializer.java +++ b/src/uws/job/serializer/XMLSerializer.java @@ -16,8 +16,8 @@ package uws.job.serializer; * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), - * Astronomisches Rechen Institut (ARI) + * Copyright 2012-2017 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ import java.io.UnsupportedEncodingException; @@ -25,10 +25,12 @@ import java.net.URLEncoder; import java.util.Iterator; import uws.ISO8601Format; +import uws.UWSException; import uws.job.ErrorSummary; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; +import uws.job.jobInfo.JobInfo; import uws.job.user.JobOwner; import uws.service.UWS; import uws.service.UWSUrl; @@ -38,7 +40,7 @@ import uws.service.request.UploadFile; * Lets serializing any UWS resource in XML. * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (02/2015) + * @version 4.2 (06/2017) */ public class XMLSerializer extends UWSSerializer { private static final long serialVersionUID = 1L; @@ -90,9 +92,13 @@ public class XMLSerializer extends UWSSerializer { } /** - * <p>Gets the XML file header (xml version, encoding and the xslt style-sheet link if any).</p> - * <p>It is always called by the implementation of the UWSSerializer functions - * if their boolean parameter (<i>root</i>) is <i>true</i>.</p> + * Gets the XML file header (xml version, encoding and the xslt + * style-sheet link if any). + * + * <p> + * It is always called by the implementation of the UWSSerializer functions + * if their boolean parameter (<i>root</i>) is <i>true</i>. + * </p> * * @return The XML file header. */ @@ -104,9 +110,12 @@ public class XMLSerializer extends UWSSerializer { } /** - * Gets all UWS namespaces declarations needed for an XML representation of a UWS object. + * Gets all UWS namespaces declarations needed for an XML representation of + * a UWS object. * - * @return The UWS namespaces: <br /> (i.e. <i>= "xmlns:uws=[...] xmlns:xlink=[...] xmlns:xs=[...] xmlns:xsi=[...] xsi:schemaLocation=[...]"</i>). + * @return The UWS namespaces: <br /> (i.e. <i>= "xmlns:uws=[...] + * xmlns:xlink=[...] xmlns:xs=[...] xmlns:xsi=[...] + * xsi:schemaLocation=[...]"</i>). */ public String getUWSNamespace(){ return "xmlns=\"http://www.ivoa.net/xml/UWS/v1.0\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.ivoa.net/xml/UWS/v1.0 http://www.ivoa.net/xml/UWS/v1.0 http://www.w3.org/1999/xlink http://www.w3.org/1999/xlink.xsd http://www.w3.org/2001/XMLSchema http://www.w3.org/2001/XMLSchema.xsd\""; @@ -116,9 +125,11 @@ public class XMLSerializer extends UWSSerializer { * Gets the node attributes which declare the UWS namespace. * * @param root <i>false</i> if the attribute to serialize will be included - * in a top level serialization (for a job attribute: job), <i>true</i> otherwise. + * in a top level serialization (for a job attribute: job), + * <i>true</i> otherwise. * - * @return "" if <i>root</i> is <i>false</i>, " "+UWSNamespace otherwise. + * @return "" if <i>root</i> is <i>false</i>, " "+UWSNamespace + * otherwise. * * @see #getUWSNamespace() */ @@ -185,7 +196,7 @@ public class XMLSerializer extends UWSSerializer { } @Override - public String getJob(final UWSJob job, final boolean root){ + public String getJob(final UWSJob job, final boolean root) throws UWSException{ StringBuffer xml = new StringBuffer(root ? getHeader() : ""); String newLine = "\n\t"; @@ -212,7 +223,12 @@ public class XMLSerializer extends UWSSerializer { xml.append(newLine).append(getResults(job, false)); // errorSummary: - xml.append(newLine).append(getErrorSummary(job.getErrorSummary(), false)); + if (job.getErrorSummary() != null) + xml.append(newLine).append(getErrorSummary(job.getErrorSummary(), false)); + + // jobInfo: + if (job.getJobInfo() != null) + xml.append(newLine).append(getJobInfo(job)); tabPrefix = ""; return xml.append("\n</job>").toString(); @@ -411,7 +427,7 @@ public class XMLSerializer extends UWSSerializer { } /* NOTE: THE FOLLOWING ATTRIBUTES MAY PROVIDE USEFUL INFORMATION TO USERS, BUT THEY ARE NOT ALLOWED BY THE CURRENT UWS STANDARD. - * HOWEVER, IF, ONE DAY, THEY ARE, THE FOLLOWING LINES SHOULD BE UNCOMNENTED. + * HOWEVER, IF, ONE DAY, THEY ARE, THE FOLLOWING LINES SHOULD BE UNCOMNENTED. * * if (result.getMimeType() != null) * xml.append(" mime=\"").append(escapeXMLAttribute(result.getMimeType())).append("\""); @@ -422,6 +438,33 @@ public class XMLSerializer extends UWSSerializer { return xml.append(" />").toString(); } + /** + * Serialize into XML the {@link JobInfo} of the given job, if any. + * + * <p><b>Important note:</b> + * By default, this function wrap the XML content returned by + * {@link JobInfo#getXML(String)} inside an XML node "jobInfo". + * To change this behavior, you should overwrite this function. + * </p> + * + * @param job The job whose the jobInfo must be serialized into XML. + * + * @return The XML serialization of the given job's jobInfo, + * or an empty string if the given job has no jobInfo. + * + * @since 4.2 + */ + public String getJobInfo(final UWSJob job) throws UWSException{ + if (job.getJobInfo() != null){ + StringBuffer xml = new StringBuffer(); + xml.append(tabPrefix).append("<jobInfo>"); + xml.append("\n\t").append(tabPrefix).append(job.getJobInfo().getXML("\n\t" + tabPrefix)); + xml.append('\n').append(tabPrefix).append("</jobInfo>"); + return xml.toString(); + }else + return ""; + } + /* ************** */ /* ESCAPE METHODS */ /* ************** */ @@ -506,10 +549,12 @@ public class XMLSerializer extends UWSSerializer { * <p>Returns a legal XML character corresponding to an input character. * Certain characters are simply illegal in XML (regardless of encoding). * If the input character is legal in XML, it is returned; - * otherwise some other weird but legal character + * otherwise some other weird but legal character * (currently the inverted question mark, "\u00BF") is returned instead.</p> * - * <p><i>Note: copy of the STILTS VOSerializer.ensureLegalXml(char) function.</i></p> + * <p><i>Note: + * copy of the STILTS VOSerializer.ensureLegalXml(char) function. + * </i></p> * * @param c input character * @return legal XML character, <code>c</code> if possible @@ -520,4 +565,44 @@ public class XMLSerializer extends UWSSerializer { return ((c >= '\u0020' && c <= '\uD7FF') || (c >= '\uE000' && c <= '\uFFFD') || ((c) == 0x09 || (c) == 0x0A || (c) == 0x0D)) ? c : '\u00BF'; } + /** Regular expression for the first character of a valid XML node name. + * <p><i>Note: + * This rule comes from the XML 1.1 standard by the W3C: + * <a href="https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-NameStartChar">https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-NameStartChar</a> + * </i></p> + * @since 4.2 */ + private final static String XML_START_NODE_NAME_REGEX = ":A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\x{10000}-\\x{EFFFF}"; + + /** + * Regular expression of a whole valid XML node name. + * <p><i>Note: + * This rule comes from the XML 1.1 standard by the W3C: + * <a href="https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Name">https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-Name</a> + * </i></p> + * @since 4.2 */ + private final static String XML_NODE_NAME_REGEX = "[" + XML_START_NODE_NAME_REGEX + "][" + XML_START_NODE_NAME_REGEX + "\\-.0-9\\xB7\\u0300-\\u036F\\u203F-\\u2040]*"; + + /** + * Determine whether the given name is a valid XML node name + * according to the W3C (XML 1.1). + * + * <p><i>Note: + * In addition of validating the given name against the regular expression + * provided by the W3C (see {@link #XML_NODE_NAME_REGEX}), this function + * ensures the given name does not start with "XML" according to the + * following W3C note: + * <a href="https://www.w3.org/TR/2006/REC-xml11-20060816/#dt-name">https://www.w3.org/TR/2006/REC-xml11-20060816/#dt-name</a> + * </i></p> + * + * @param nodeName XML node name to test. + * + * @return <code>true</code> if the given node name is valid, + * <code>false</code> otherwise. + * + * @since 4.2 + */ + public static boolean isValidXMLNodeName(final String nodeName){ + return nodeName.matches(XML_NODE_NAME_REGEX) && !nodeName.toLowerCase().startsWith("xml"); + } + } diff --git a/src/uws/service/AbstractUWSFactory.java b/src/uws/service/AbstractUWSFactory.java index 332254a9014638dbc5c35795765ed544b394986d..d066712ee3c6afe8fcbebdde7a0fe65b1164ca0c 100644 --- a/src/uws/service/AbstractUWSFactory.java +++ b/src/uws/service/AbstractUWSFactory.java @@ -16,7 +16,7 @@ package uws.service; * 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-2016 - 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) */ @@ -33,6 +33,7 @@ import uws.job.ErrorSummary; import uws.job.JobThread; import uws.job.Result; import uws.job.UWSJob; +import uws.job.jobInfo.JobInfo; import uws.job.parameters.DestructionTimeController; import uws.job.parameters.DestructionTimeController.DateField; import uws.job.parameters.ExecutionDurationController; @@ -48,7 +49,7 @@ import uws.service.request.UWSRequestParser; * Only the function which creates a {@link JobThread} from a {@link UWSJob} needs to be implemented.</p> * * @author Grégory Mantelet (CDS;ARI) - * @version 4.2 (01/2016) + * @version 4.2 (06/2017) */ public abstract class AbstractUWSFactory implements UWSFactory { @@ -84,13 +85,20 @@ public abstract class AbstractUWSFactory implements UWSFactory { @Override public UWSJob createJob(final HttpServletRequest request, final JobOwner user) throws UWSException{ - // Extract the HTTP request ID (the job ID should be the same, if not already used by another job): + // Extract the HTTP request ID (the job ID should be the same, if not already used by another job): String requestID = null; if (request != null && request.getAttribute(UWS.REQ_ATTRIBUTE_ID) != null && request.getAttribute(UWS.REQ_ATTRIBUTE_ID) instanceof String) requestID = request.getAttribute(UWS.REQ_ATTRIBUTE_ID).toString(); // Create the job: - return new UWSJob(user, createUWSParameters(request), requestID); + UWSJob newJob = new UWSJob(user, createUWSParameters(request), requestID); + + // Set the XML job description if any: + Object jobDesc = request.getAttribute(UWS.REQ_ATTRIBUTE_JOB_DESCRIPTION); + if (jobDesc != null && jobDesc instanceof JobInfo) + newJob.setJobInfo((JobInfo)jobDesc); + + return newJob; } @Override diff --git a/src/uws/service/UWS.java b/src/uws/service/UWS.java index 09d5e2456b4ab128bd2b56f1f6ff0ac7f046d658..a8d8bab6daafb61d5b7dde3795bc5bd7c7188985 100644 --- a/src/uws/service/UWS.java +++ b/src/uws/service/UWS.java @@ -16,7 +16,7 @@ package uws.service; * 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) */ @@ -64,7 +64,7 @@ import uws.service.request.UWSRequestParser; * </b></p> * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (02/2015) + * @version 4.2 (06/2017) */ public interface UWS extends Iterable<JobList> { @@ -80,6 +80,11 @@ public interface UWS extends Iterable<JobList> { * @since 4.1 */ public static final String REQ_ATTRIBUTE_USER = "UWS_USER"; + /** Attribute of the HttpServletRequest to set and to get in order to access the Job-Description (generally in XML) + * sent instead of the "normal" HTTP-POST/-PUT parameters in the HTTP request body. + * @since 4.2 */ + public static final String REQ_ATTRIBUTE_JOB_DESCRIPTION = "UWS_JOB_DESCRIPTION"; + /** * Gets the name of this UWS. * @@ -105,7 +110,7 @@ public interface UWS extends Iterable<JobList> { * In brief, this function should release all used resources. * </p> * - * <p><b>IMPORTANT: This function should be called only when the JVM or the Web Application Server is stopping.</b></p> + * <p><b>IMPORTANT: This function should be called only when the JVM or the Web Application Server is stopping.</b></p> * * <p><i>Note: * A call to this function may prevent this instance of {@link UWS} to execute any subsequent HTTP request, or the behavior diff --git a/src/uws/service/UWSServlet.java b/src/uws/service/UWSServlet.java index 2b1fd209d98906162961926afeaf021f0ee3d5a4..d7660548f7c7713c6952c2b067240f38190818aa 100644 --- a/src/uws/service/UWSServlet.java +++ b/src/uws/service/UWSServlet.java @@ -16,7 +16,7 @@ package uws.service; * 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-2016 - 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) */ @@ -48,6 +48,7 @@ import uws.job.JobList; import uws.job.JobThread; import uws.job.Result; import uws.job.UWSJob; +import uws.job.jobInfo.JobInfo; import uws.job.parameters.DestructionTimeController; import uws.job.parameters.DestructionTimeController.DateField; import uws.job.parameters.ExecutionDurationController; @@ -102,7 +103,7 @@ import uws.service.request.UploadFile; * addJobList(new JobList("jobList")); * } * - * // Create the job process corresponding to the job to execute ; generally, the process identification can be merely done by checking the job list name. + * // Create the job process corresponding to the job to execute ; generally, the process identification can be merely done by checking the job list name. * public JobThread createJobThread(UWSJob job) throws UWSException { * if (job.getJobList().getName().equals("jobList")) * return new MyJobThread(job); @@ -149,7 +150,7 @@ import uws.service.request.UploadFile; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 4.2 (02/2016) + * @version 4.2 (06/2017) */ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory { private static final long serialVersionUID = 1L; @@ -467,7 +468,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory /* * Any known/"expected" UWS exception is logged but also returned to the HTTP client in an error document. * Since the error is known, it is supposed to have already been logged with a full stack trace. Thus, there - * is no need to log again its stack trace...just its message is logged. + * is no need to log again its stack trace...just its message is logged. * Besides, this error may also be just a redirection and not a true error. In such case, the error message * is not logged. */ @@ -480,7 +481,7 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory * If this exception happens, the library tried to rewrite the HTTP response body with a message or a result, * while this body has already been partially sent to the client. It is then no longer possible to change its content. * Consequently, the error is logged as FATAL and a message will be appended at the end of the already submitted response - * to alert the HTTP client that an error occurs and the response should not be considered as complete and reliable. + * to alert the HTTP client that an error occurs and the response should not be considered as complete and reliable. */ // Write the error in the response and return the appropriate HTTP status code: errorWriter.writeError(ise, resp, req, reqID, user, uwsAction); @@ -643,7 +644,8 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory input.close(); } } - }// ERROR DETAILS CASE: Display the full stack trace of the error: + } + // ERROR DETAILS CASE: Display the full stack trace of the error: else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY) && attributes.length > 1 && attributes[1].equalsIgnoreCase("details")){ ErrorSummary error = job.getErrorSummary(); if (error == null) @@ -661,7 +663,16 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory input.close(); } } - }// REFERENCE FILE: Display the content of the uploaded file or redirect to the URL (if it is a URL): + } + // JOB INFO: Display the content of the JobInfo field (if any): + else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_JOB_INFO)){ + + if (job.getJobInfo() == null) + resp.sendError(HttpServletResponse.SC_NO_CONTENT); + else + job.getJobInfo().write(resp); + } + // REFERENCE FILE: Display the content of the uploaded file or redirect to the URL (if it is a URL): else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS) && attributes.length > 1 && job.getAdditionalParameterValue(attributes[1]) != null && job.getAdditionalParameterValue(attributes[1]) instanceof UploadFile){ UploadFile upl = (UploadFile)job.getAdditionalParameterValue(attributes[1]); if (upl.getLocation().matches("^http(s)?://")) @@ -752,7 +763,15 @@ public abstract class UWSServlet extends HttpServlet implements UWS, UWSFactory @Override public UWSJob createJob(HttpServletRequest request, JobOwner user) throws UWSException{ - return new UWSJob(user, createUWSParameters(request)); + // Create the job: + UWSJob newJob = new UWSJob(user, createUWSParameters(request)); + + // Set the XML job description if any: + Object jobDesc = request.getAttribute(UWS.REQ_ATTRIBUTE_JOB_DESCRIPTION); + if (jobDesc != null && jobDesc instanceof JobInfo) + newJob.setJobInfo((JobInfo)jobDesc); + + return newJob; } @Override diff --git a/src/uws/service/actions/GetJobParam.java b/src/uws/service/actions/GetJobParam.java index be56b1ad5074d577a4d56c65d74867d497537257..ca38cfcd265eb8f19101ee5ba3039de3852f48ed 100644 --- a/src/uws/service/actions/GetJobParam.java +++ b/src/uws/service/actions/GetJobParam.java @@ -16,7 +16,7 @@ package uws.service.actions; * 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) */ @@ -32,6 +32,7 @@ import uws.UWSToolBox; import uws.job.ErrorSummary; import uws.job.Result; import uws.job.UWSJob; +import uws.job.jobInfo.JobInfo; import uws.job.serializer.UWSSerializer; import uws.job.user.JobOwner; import uws.service.UWSService; @@ -50,7 +51,7 @@ import uws.service.request.UploadFile; * The serializer is choosen in function of the HTTP Accept header.</p> * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (04/2015) + * @version 4.2 (06/2017) */ public class GetJobParam extends UWSAction { private static final long serialVersionUID = 1L; @@ -99,6 +100,7 @@ public class GetJobParam extends UWSAction { * @see #getJob(UWSUrl) * @see UWSService#getSerializer(String) * @see UWSJob#serialize(ServletOutputStream, UWSSerializer) + * @see JobInfo#write(HttpServletResponse) * * @see uws.service.actions.UWSAction#apply(UWSUrl, JobOwner, HttpServletRequest, HttpServletResponse) */ @@ -129,7 +131,8 @@ public class GetJobParam extends UWSAction { input.close(); } } - }// ERROR DETAILS CASE: Display the full stack trace of the error: + } + // ERROR DETAILS CASE: Display the full stack trace of the error: else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY) && attributes.length > 1 && attributes[1].equalsIgnoreCase("details")){ ErrorSummary error = job.getErrorSummary(); if (error == null) @@ -147,7 +150,15 @@ public class GetJobParam extends UWSAction { input.close(); } } - }// REFERENCE FILE: Display the content of the uploaded file or redirect to the URL (if it is a URL): + } + // JOB INFO: Display the content of the JobInfo field (if any): + else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_JOB_INFO)){ + if (job.getJobInfo() == null) + response.sendError(HttpServletResponse.SC_NO_CONTENT); + else + job.getJobInfo().write(response); + } + // REFERENCE FILE: Display the content of the uploaded file or redirect to the URL (if it is a URL): else if (attributes[0].equalsIgnoreCase(UWSJob.PARAM_PARAMETERS) && attributes.length > 1 && job.getAdditionalParameterValue(attributes[1]) != null && job.getAdditionalParameterValue(attributes[1]) instanceof UploadFile){ UploadFile upl = (UploadFile)job.getAdditionalParameterValue(attributes[1]); if (upl.getLocation().matches("^http(s)?://")) @@ -165,7 +176,8 @@ public class GetJobParam extends UWSAction { input.close(); } } - }// DEFAULT CASE: Display the serialization of the selected UWS object: + } + // DEFAULT CASE: Display the serialization of the selected UWS object: else{ // Write the value/content of the selected attribute: UWSSerializer serializer = uws.getSerializer(request.getHeader("Accept")); diff --git a/src/uws/service/backup/DefaultUWSBackupManager.java b/src/uws/service/backup/DefaultUWSBackupManager.java index 22393f81868e0b9e3e9b9901db83f4580be38e76..d749be5f45692539b4cc2d3aee527560a77dc785 100644 --- a/src/uws/service/backup/DefaultUWSBackupManager.java +++ b/src/uws/service/backup/DefaultUWSBackupManager.java @@ -16,14 +16,19 @@ package uws.service.backup; * 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,2014 - 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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.PrintWriter; +import java.io.Serializable; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; @@ -41,6 +46,9 @@ import org.json.JSONTokener; import org.json.JSONWriter; import org.json.Json4Uws; +import com.oreilly.servlet.Base64Decoder; +import com.oreilly.servlet.Base64Encoder; + import uws.ISO8601Format; import uws.UWSException; import uws.UWSToolBox; @@ -49,6 +57,7 @@ import uws.job.ErrorType; import uws.job.JobList; import uws.job.Result; import uws.job.UWSJob; +import uws.job.jobInfo.JobInfo; import uws.job.parameters.UWSParameters; import uws.job.user.JobOwner; import uws.service.UWS; @@ -77,7 +86,7 @@ import uws.service.request.UploadFile; * <p>Another positive value will be considered as the frequency (in milliseconds) of the automatic backup (= {@link #saveAll()}).</p> * * @author Grégory Mantelet (CDS;ARI) - * @version 4.1 (12/2014) + * @version 4.2 (06/2017) */ public class DefaultUWSBackupManager implements UWSBackupManager { @@ -571,9 +580,92 @@ public class DefaultUWSBackupManager implements UWSBackupManager { // Add the name of the job list owning the given job: jsonJob.put("jobListName", jlName); + // ReSet jobInfo to a boolean field: + if (job.getJobInfo() != null) + jsonJob.put(UWSJob.PARAM_JOB_INFO, getJSONJobInfo(job.getJobInfo())); + else + jsonJob.remove(UWSJob.PARAM_JOB_INFO); + return jsonJob; } + /** + * Serialize the given {@link JobInfo} so that being able later to restore this exact object as provided. + * + * <p><i> + * By default, this function use the Java Class serialization (see {@link Serializable}) + * and save the corresponding bytes into a Base-64 string. + * </i></p> + * + * @param jobInfo The jobInfo to backup. + * + * @return The string to use in order to restore the given jobInfo + * (e.g. a Base-64 serialization of the Java Object, a URL, ...). + * + * @throws UWSException If any error occurs while representing the given {@link JobInfo}. + * @throws JSONException If any error occurs while manipulating a JSON object or array. + * + * @since 4.2 + */ + protected Object getJSONJobInfo(final JobInfo jobInfo) throws UWSException, JSONException{ + ByteArrayOutputStream bArray = null; + ObjectOutputStream oOutput = null; + try{ + bArray = new ByteArrayOutputStream(); + oOutput = new ObjectOutputStream(bArray); + oOutput.writeObject(jobInfo); + oOutput.flush(); + return Base64Encoder.encode(bArray.toByteArray()); + }catch(IOException ioe){ + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Unexpected error while serializing the given JobInfo!"); + }finally{ + if (oOutput != null){ + try{ + oOutput.close(); + }catch(IOException ioe){} + } + if (bArray != null){ + try{ + bArray.close(); + }catch(IOException ioe){} + } + } + } + + /** + * Restore the JobInfo referenced or represented by the given JSON value. + * + * <p><i> + * By default, this function considers that the given value is a Base-64 string encoding + * the Java Class serialization (see {@link Serializable}) of the {@link JobInfo} to restore. + * </i></p> + * + * @param jsonValue The reference or backup representation of the {@link JobInfo} to restore. + * + * @return The restored {@link JobInfo}. + * + * @throws UWSException If any error occurs while restoring the {@link JobInfo}. + * @throws JSONException If any error occurs while manipulating a JSON object or array. + * + * @since 4.2 + */ + protected JobInfo restoreJobInfo(final Object jsonValue) throws UWSException, JSONException{ + ObjectInputStream oInput = null; + try{ + byte[] bArray = Base64Decoder.decodeToBytes((String)jsonValue); + oInput = new ObjectInputStream(new ByteArrayInputStream(bArray)); + return (JobInfo)oInput.readObject(); + }catch(Exception ex){ + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ex, "Unexpected error while restoring a JobInfo!"); + }finally{ + if (oInput != null){ + try{ + oInput.close(); + }catch(IOException ioe){} + } + } + } + /** * Get the JSON representation of the given {@link UploadFile}. * @@ -817,12 +909,15 @@ public class DefaultUWSBackupManager implements UWSBackupManager { String jobListName = null, jobId = null, ownerID = null, tmp; //Date destruction=null; - long quote = UWSJob.UNLIMITED_DURATION, /*duration = UWSJob.UNLIMITED_DURATION, */startTime = -1, endTime = -1; + long quote = UWSJob.UNLIMITED_DURATION, + /*duration = UWSJob.UNLIMITED_DURATION, */startTime = -1, + endTime = -1; HashMap<String,Object> inputParams = new HashMap<String,Object>(10); //Map<String, Object> params = null; ArrayList<Result> results = null; ErrorSummary error = null; JSONArray uploads = null; + JobInfo jobInfo = null; String[] keys = JSONObject.getNames(json); for(String key : keys){ @@ -902,6 +997,11 @@ public class DefaultUWSBackupManager implements UWSBackupManager { else if (key.equalsIgnoreCase(UWSJob.PARAM_ERROR_SUMMARY)){ error = getError(json.getJSONObject(key)); + } + // key=JOB_INFO: + else if (key.equalsIgnoreCase(UWSJob.PARAM_JOB_INFO)){ + jobInfo = restoreJobInfo(json.get(key)); + }// Ignore any other key but with a warning message: else getLogger().logUWS(LogLevel.WARNING, json, "RESTORATION", "The job attribute '" + key + "' has been ignored because unknown! A job may be not completely restored!", null); @@ -962,6 +1062,10 @@ public class DefaultUWSBackupManager implements UWSBackupManager { // Create the job: UWSJob job = uws.getFactory().createJob(jobId, owner, uwsParams, quote, startTime, endTime, results, error); + // Set its jobInfo, if any: + if (jobInfo != null) + job.setJobInfo(jobInfo); + // Restore other job params if needed: restoreOtherJobParams(json, job); diff --git a/src/uws/service/request/UWSRequestParser.java b/src/uws/service/request/UWSRequestParser.java index 89ea7d9eb3d41d159d9e2d9dae1a9337077cf4b8..18832ade54cfffffec4a46f9491001e2ab788e3f 100644 --- a/src/uws/service/request/UWSRequestParser.java +++ b/src/uws/service/request/UWSRequestParser.java @@ -16,7 +16,7 @@ package uws.service.request; * 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 2014 - Astronomisches Rechen Institut (ARI) + * Copyright 2014-2017 - Astronomisches Rechen Institut (ARI) */ import java.util.HashMap; @@ -29,50 +29,64 @@ import uws.UWSToolBox; import uws.service.file.UWSFileManager; /** - * <p>This parser adapts the request parser to use in function of the request content-type:</p> + * This parser adapts the request parser to use in function of the request + * content-type: + * * <ul> * <li><b>application/x-www-form-urlencoded</b>: {@link FormEncodedParser}</li> * <li><b>multipart/form-data</b>: {@link MultipartParser}</li> - * <li><b>other</b>: {@link NoEncodingParser} (the whole request body will be stored as one single parameter)</li> + * <li><b>(text|application)/(.+-)?xml</b>: {@link XMLRequestParser} + * (the whole request body is an XML document)</li> + * <li><b>other</b>: no parameter is returned</li> * </ul> * * <p> - * The request body size is limited for the multipart AND the no-encoding parsers. If you want to change this limit, - * you MUST do it for each of these parsers, setting the following static attributes: resp. {@link MultipartParser#SIZE_LIMIT} - * and {@link NoEncodingParser#SIZE_LIMIT}. - * </p> + * The request body size is limited for the multipart AND the XML-Request + * parsers. If you want to change this limit, you MUST do it for each of these + * parsers, setting the following static attributes: resp. + * {@link MultipartParser#SIZE_LIMIT} and {@link XMLRequestParser#SIZE_LIMIT} + * (and also {@link XMLRequestParser#SMALL_XML_THRESHOLD}). + * </p> * * <p><i>Note: - * If you want to change the support other request parsing, you will have to write your own {@link RequestParser} implementation. + * If you want to change the support other request parsing, you will have to + * write your own {@link RequestParser} implementation. * </i></p> * * @author Grégory Mantelet (ARI) - * @version 4.1 (12/2014) + * @version 4.2 (06/2017) * @since 4.1 */ public final class UWSRequestParser implements RequestParser { /** File manager to use to create {@link UploadFile} instances. - * It is required by this new object to execute open, move and delete operations whenever it could be asked. */ + * It is required by this new object to execute open, move and delete + * operations whenever it could be asked. */ private final UWSFileManager fileManager; - /** {@link RequestParser} to use when a application/x-www-form-urlencoded request must be parsed. This attribute is set by {@link #parse(HttpServletRequest)} - * only when needed, by calling the function {@link #getFormParser()}. */ + /** {@link RequestParser} to use when a application/x-www-form-urlencoded + * request must be parsed. This attribute is set by + * {@link #parse(HttpServletRequest)} only when needed, by calling the + * function {@link #getFormParser()}. */ private RequestParser formParser = null; - /** {@link RequestParser} to use when a multipart/form-data request must be parsed. This attribute is set by {@link #parse(HttpServletRequest)} + /** {@link RequestParser} to use when a multipart/form-data request must be + * parsed. This attribute is set by {@link #parse(HttpServletRequest)} * only when needed, by calling the function {@link #getMultipartParser()}. */ private RequestParser multipartParser = null; - /** {@link RequestParser} to use when none of the other parsers can be used ; it will then transform the whole request body in a parameter called "JDL" - * (Job Description Language). This attribute is set by {@link #parse(HttpServletRequest)} only when needed, by calling the function - * {@link #getNoEncodingParser()}. */ - private RequestParser noEncodingParser = null; + /** {@link RequestParser} to use for XML request (i.e. a HTTP request + * containing just an XML document). This attribute is set by + * {@link #parse(HttpServletRequest)} only when needed, by calling the + * function {@link #getXMLRequestParser()}. */ + private RequestParser xmlRequestParser = null; /** - * Build a {@link RequestParser} able to choose the most appropriate {@link RequestParser} in function of the request content-type. + * Build a {@link RequestParser} able to choose the most appropriate + * {@link RequestParser} in function of the request content-type. * - * @param fileManager The file manager to use in order to store any eventual upload. <b>MUST NOT be NULL</b> + * @param fileManager The file manager to use in order to store any + * eventual upload. <i>Must NOT be NULL.</i> */ public UWSRequestParser(final UWSFileManager fileManager){ if (fileManager == null) @@ -96,8 +110,10 @@ public final class UWSRequestParser implements RequestParser { params = getFormParser().parse(req); else if (MultipartParser.isMultipartContent(req)) params = getMultipartParser().parse(req); + else if (XMLRequestParser.isXMLRequest(req)) + params = getXMLRequestParser().parse(req); else - params = getNoEncodingParser().parse(req); + params = new HashMap<String,Object>(0); // Only for POST requests, the parameters specified in the URL must be added: if (method.equals("post")) @@ -109,33 +125,41 @@ public final class UWSRequestParser implements RequestParser { } /** - * Get the {@link RequestParser} to use for application/x-www-form-urlencoded HTTP requests. + * Get the {@link RequestParser} to use for + * application/x-www-form-urlencoded HTTP requests. * This parser may be created if not already done. * - * @return The {@link RequestParser} to use for application/x-www-form-urlencoded requests. <i>Never NULL</i> + * @return The {@link RequestParser} to use for + * application/x-www-form-urlencoded requests. <i>Never NULL</i> */ private synchronized final RequestParser getFormParser(){ return (formParser == null) ? (formParser = new FormEncodedParser()) : formParser; } /** - * Get the {@link RequestParser} to use for multipart/form-data HTTP requests. + * Get the {@link RequestParser} to use for multipart/form-data HTTP + * requests. * This parser may be created if not already done. * - * @return The {@link RequestParser} to use for multipart/form-data requests. <i>Never NULL</i> + * @return The {@link RequestParser} to use for multipart/form-data + * requests. <i>Never NULL</i> */ private synchronized final RequestParser getMultipartParser(){ return (multipartParser == null) ? (multipartParser = new MultipartParser(fileManager)) : multipartParser; } /** - * Get the {@link RequestParser} to use for HTTP requests whose the content type is neither application/x-www-form-urlencoded nor multipart/form-data. + * Get the {@link RequestParser} to use for HTTP requests whose the content + * is an XML document. * This parser may be created if not already done. * - * @return The {@link RequestParser} to use for requests whose the content-type is not supported. <i>Never NULL</i> + * @return The {@link RequestParser} to use for XML requests. + * <i>Never NULL</i> + * + * @since 4.2 */ - private synchronized final RequestParser getNoEncodingParser(){ - return (noEncodingParser == null) ? (noEncodingParser = new NoEncodingParser(fileManager)) : noEncodingParser; + private synchronized final RequestParser getXMLRequestParser(){ + return (xmlRequestParser == null) ? (xmlRequestParser = new XMLRequestParser(fileManager)) : xmlRequestParser; } } diff --git a/src/uws/service/request/XMLRequestParser.java b/src/uws/service/request/XMLRequestParser.java new file mode 100644 index 0000000000000000000000000000000000000000..448ebbcbbfa295bb07b55e03448d87217c544d74 --- /dev/null +++ b/src/uws/service/request/XMLRequestParser.java @@ -0,0 +1,372 @@ +package uws.service.request; + +/* + * 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.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import uws.UWSException; +import uws.job.jobInfo.XMLJobInfo; +import uws.service.UWS; +import uws.service.file.UWSFileManager; + +/** + * This parser aims to copy the full content of an HTTP request if it is + * identified as an XML document. + * + * <p><b>UWS's Job Description</b></p> + * + * <p> + * Actually, this parser implements the possibility defined in the UWS 1.0 + * standard to provide an XML document describing the parameters of a UWS job. + * This XML document is then called "Job Description". + * </p> + * + * <p><b>Validation</b></p> + * + * <p> + * In the UWS 1.0 standard, it is said that this Job Description has to follow + * a Job Description Language (JDL ; that's to say a known pattern describing + * the expected job parameters) dependent of the UWS service implementation. + * </p> + * + * <p> + * By default, this parser copies the request content and checks it is an XML + * document. Nothing else is done, and particularly not the validation of + * its content. To do so, a particular service implementation can extend this + * class and overwrite its function {@link #validate(InputStream)}. By default + * this function just ensures the request content is a valid XML document. + * </p> + * + * <p><b>Document access</b></p> + * + * <p> + * Once parsed, the request content will be made accessible through an + * {@link HttpServletRequest} attribute under the name + * <b>{@value uws.service.UWS#REQ_ATTRIBUTE_JOB_DESCRIPTION}</b>. + * The associated object <b>is typed as an {@link XMLJobInfo}</b>. + * </p> + * + * <p><i>Note: + * Afterwards, it is intended to be attached to a {@link uws.job.UWSJob} and + * then made accessible through its function + * {@link uws.job.UWSJob#getJobInfo()}. + * </i></p> + * + * <p><b>Document storage</b></p> + * + * <p>{@link XMLJobInfo} gives two storage possibility:</p> + * <ol> + * <li><i>in memory</i> with the constructor + * {@link XMLJobInfo#XMLJobInfo(String) XMLJobInfo(String)}</li> + * <li><i>in a file</i> with the constructor + * {@link XMLJobInfo#XMLJobInfo(UploadFile) XMLJobInfo(UploadFile)}</li> + * </ol> + * + * <p> + * The storage chosen by this parser depends on the size of the input document. + * If it exceeds {@link #SMALL_XML_THRESHOLD} (expressed in bytes), then + * the document will be stored inside a file. Otherwise it will be kept in + * memory. To change this threshold, it is just needed to set the static + * field {@link #SMALL_XML_THRESHOLD} to the desired value (in bytes). + * By default, it is set to {@value #DEFAULT_SMALL_XML_THRESHOLD} bytes. + * </p> + * + * <p><b>Important:</b> + * It is possible to prevent the unwanted storage of a very large document + * by setting the limit {@link #SIZE_LIMIT} to a different value (in bytes). + * If the input document exceeds this size, the request will be rejected with + * an 413 (REQUEST ENTITY TOO LARGE) error. + * By default this limit is set to {@value #DEFAULT_SIZE_LIMIT} bytes. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 4.2 (06/2017) + * @since 4.2 + */ +public class XMLRequestParser implements RequestParser { + + /** Default maximum allowed size for an HTTP request content: 200 kiB. */ + public static final int DEFAULT_SIZE_LIMIT = 200 * 1024; + + /** <p>Maximum allowed size for an HTTP request content. Over this limit, an exception is thrown and the request is aborted.</p> + * <p><i>Note: + * The default value is {@link #DEFAULT_SIZE_LIMIT} (= {@value #DEFAULT_SIZE_LIMIT} Bytes). + * </i></p> + * <p><i>Note: + * This limit is expressed in bytes and can not be negative. + * Its smallest possible value is 0. If the set value is though negative, + * it will be ignored and {@link #DEFAULT_SIZE_LIMIT} will be used instead. + * </i></p> */ + public static int SIZE_LIMIT = DEFAULT_SIZE_LIMIT; + + /** Default threshold for XML document that can be kept entirely in memory: 2 kiB. */ + public static final int DEFAULT_SMALL_XML_THRESHOLD = 2 * 1024; + + /** This threshold determines whether an XML request content should be + * stored in memory or inside a file. + * <p><i>In short: between 0 and this value, the + * XML document will be stored in memory ; above this value, it will be + * stored in a file.</i></p> */ + public static int SMALL_XML_THRESHOLD = DEFAULT_SMALL_XML_THRESHOLD; + + /** File manager to use to create {@link UploadFile} instances. + * It is required by this new object to execute open, move and delete operations whenever it could be asked. */ + protected final UWSFileManager fileManager; + + /** + * Build the request parser. + * + * @param fileManager A file manager. <i>Must NOT be NULL.</i> + */ + public XMLRequestParser(final UWSFileManager fileManager){ + if (fileManager == null) + throw new NullPointerException("Missing file manager => can not create an XMLRequestParser!"); + this.fileManager = fileManager; + } + + @Override + public Map<String,Object> parse(final HttpServletRequest request) throws UWSException{ + // Result of the request parsing => a JobInfo containing or pointing toward the sent request content: + XMLJobInfo jobDesc = null; + + // Prepare to write a file if the XML is too large to fit in memory: + // (note: this file has to be deleted if not used or in case or error) + Object reqID = request.getAttribute(UWS.REQ_ATTRIBUTE_ID); + if (reqID == null || !(reqID instanceof String)) + reqID = (new Date()).getTime(); + File xmlFile = new File(UWSFileManager.TMP_UPLOAD_DIR, "JOB_DESCRIPTION_" + reqID); + + OutputStream output = null; + InputStream input = null; + long totalLength = 0; + try{ + // prepare the reading of the HTTP request body: + input = new BufferedInputStream(request.getInputStream()); + + // open in WRITE access the output file: + output = new BufferedOutputStream(new FileOutputStream(xmlFile)); + + // compute the maximum limit and the memory size threshold: + final int maxSize = (SIZE_LIMIT < 0 ? DEFAULT_SIZE_LIMIT : SIZE_LIMIT); + final int memoryThreshold = (SMALL_XML_THRESHOLD < 0 ? DEFAULT_SMALL_XML_THRESHOLD : SMALL_XML_THRESHOLD); + final String tooLargeErrorMsg = "XML document too large (>" + maxSize + " bytes) => Request rejected! You should see with the service administrator to extend this limit."; + + // Start reading the HTTP request body: + byte[] buffer = new byte[memoryThreshold + 1]; + int len = input.read(buffer); + + // If nothing, no body and no parameter => stop here immediately: + if (len <= 0){ + output.close(); + output = null; + xmlFile.delete(); + } + // If the HTTP request body is already finished => small document => memory storage: + else if (len <= memoryThreshold){ + output.close(); + output = null; + xmlFile.delete(); + if (len > maxSize) + throw new UWSException(UWSException.REQUEST_ENTITY_TOO_LARGE, tooLargeErrorMsg); + else{ + // Build the corresponding String: + String smallXML = new String(buffer, 0, len, (request.getCharacterEncoding() != null ? request.getCharacterEncoding() : "UTF-8")); + + // Check it is really an XML document: + validate(smallXML); + + // Finally build the corresponding Job-Description: + jobDesc = new XMLJobInfo(smallXML); + } + } + // Otherwise.... + else{ + // ...store the full content inside the temporary file + // until the EOF or a length exceed: + do{ + output.write(buffer, 0, len); + totalLength += len; + // if content too large => stop here with an error: + if (totalLength > maxSize){ + output.close(); + output = null; + xmlFile.delete(); + throw new UWSException(UWSException.REQUEST_ENTITY_TOO_LARGE, tooLargeErrorMsg); + } + }while((len = input.read(buffer)) > 0); + output.flush(); + output.close(); + output = null; + + // Check the file is really an XML document: + validate(xmlFile); + + // Create a UWS wrapping for this uploaded file: + UploadFile xmlUpload = new UploadFile(UWS.REQ_ATTRIBUTE_JOB_DESCRIPTION, xmlFile.toURI().toString(), fileManager); + xmlUpload.mimeType = request.getContentType(); + xmlUpload.length = totalLength; + + // And create the corresponding jobInfo: + jobDesc = new XMLJobInfo(xmlUpload); + } + }catch(IOException ioe){ + xmlFile.delete(); + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Internal error => Impossible to get the XML document from the HTTP request!"); + }catch(UWSException ue){ + xmlFile.delete(); + throw ue; + }finally{ + if (output != null){ + try{ + output.close(); + }catch(IOException ioe2){} + } + if (input != null){ + try{ + input.close(); + }catch(IOException ioe2){} + } + } + + // Put the job description in a HttpServletRequest attribute: + if (jobDesc != null) + request.setAttribute(UWS.REQ_ATTRIBUTE_JOB_DESCRIPTION, jobDesc); + + // Return an empty map => no parameter has been directly provided: + return new HashMap<String,Object>(0); + } + + /** + * Validate the given XML document. + * + * <p> + * By default, it is only ensured this document is an XML one. + * </p> + * + * @param smallXML The document to check. + * + * @throws UWSException If the given document is not valid. + * + * @see {@link #validate(InputStream)} + */ + protected void validate(final String smallXML) throws UWSException{ + validate(new ByteArrayInputStream(smallXML.getBytes())); + } + + /** + * Validate the specified XML document. + * + * <p> + * By default, it is only ensured this document is an XML one. + * </p> + * + * @param xmlFile The file containing the document to check. + * + * @throws UWSException If the specified document is not valid. + * + * @see {@link #validate(InputStream)} + */ + protected void validate(final File xmlFile) throws UWSException{ + InputStream input = null; + try{ + input = new FileInputStream(xmlFile); + validate(input); + }catch(IOException ioe){ + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe); + }finally{ + if (input != null){ + try{ + input.close(); + }catch(IOException ioe){} + } + } + } + + /** + * Validate the given XML document. + * + * <p> + * By default, it is only ensured this document is an XML one. + * </p> + * + * @param input Stream toward the document to check. + * + * @throws UWSException If the given document is not valid. + */ + protected void validate(final InputStream input) throws UWSException{ + SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setNamespaceAware(true); // why not :) + spf.setValidating(false); // no need to check the DTD or XSDs + SAXParser saxParser = null; + try{ + saxParser = spf.newSAXParser(); + saxParser.parse(input, new DefaultHandler()); + }catch(SAXParseException spe){ + throw new UWSException(UWSException.BAD_REQUEST, "Incorrect XML input! ERROR at [l." + spe.getLineNumber() + ", c." + spe.getColumnNumber() + "]: " + spe.getMessage() + "."); + }catch(Exception se){ + throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, se); + } + } + + /** + * Utility method that determines whether the content of the given + * request is an XML (or XML-derived) document. + * + * <p><b>Important:</b> + * This function just tests the content-type of the request. + * Neither the HTTP method (e.g. GET, POST, ...) nor the content is tested. + * </p> + * + * @param request The servlet request to be evaluated. + * <i>Must NOT be NULL.</i> + * + * @return <i>true</i> if the request is an XML document, + * <i>false</i> otherwise. + */ + public final static boolean isXMLRequest(final HttpServletRequest request){ + // Extract the content type and determine if it is an XML request: + String contentType = request.getContentType(); + if (contentType == null) + return false; + else if (contentType.toLowerCase().matches("(text|application)/(.+\\+)?xml")) + return true; + else + return false; + } + +}