You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@taverna.apache.org by st...@apache.org on 2018/01/09 23:30:41 UTC

[25/42] incubator-taverna-server git commit: package org.taverna -> org.apache.taverna

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/TavernaServerRunREST.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/TavernaServerRunREST.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/TavernaServerRunREST.java
new file mode 100644
index 0000000..ef6ddd4
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/TavernaServerRunREST.java
@@ -0,0 +1,810 @@
+/*
+ */
+package org.taverna.server.master.rest;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static org.joda.time.format.ISODateTimeFormat.basicDateTime;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.handler.Scufl2DocumentHandler.SCUFL2;
+import static org.taverna.server.master.interaction.InteractionFeedSupport.FEED_URL_DIR;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.ROBUNDLE;
+import static org.taverna.server.master.rest.ContentTypes.TEXT;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.DIR;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.GENERATE_PROVENANCE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.IN;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.LISTEN;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.LOG;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.NAME;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.OUT;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.PROFILE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.ROOT;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.RUNBUNDLE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.SEC;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.STATUS;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.STDERR;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.STDOUT;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.T_CREATE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.T_EXPIRE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.T_FINISH;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.T_START;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.USAGE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.WF;
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.joda.time.format.DateTimeFormatter;
+import org.taverna.server.master.common.Namespaces;
+import org.taverna.server.master.common.ProfileList;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.NotOwnerException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.port_description.OutputDescription;
+
+/**
+ * This represents how a Taverna Server workflow run looks to a RESTful API.
+ * 
+ * @author Donal Fellows.
+ */
+@Description("This represents how a Taverna Server workflow run looks to a "
+		+ "RESTful API.")
+@RolesAllowed(USER)
+public interface TavernaServerRunREST {
+	/**
+	 * Describes a workflow run.
+	 * 
+	 * @param ui
+	 *            About the URI used to access this resource.
+	 * @return The description.
+	 */
+	@GET
+	@Path(ROOT)
+	@Description("Describes a workflow run.")
+	@Produces({ XML, JSON })
+	@Nonnull
+	public RunDescription getDescription(@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Deletes a workflow run.
+	 * 
+	 * @return An HTTP response to the deletion.
+	 * @throws NoUpdateException
+	 *             If the user may see the handle but may not delete it.
+	 */
+	@DELETE
+	@Path(ROOT)
+	@Description("Deletes a workflow run.")
+	@Nonnull
+	public Response destroy() throws NoUpdateException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ROOT)
+	@Description("Produces the description of the run.")
+	Response runOptions();
+
+	/**
+	 * Returns the workflow document used to create the workflow run.
+	 * 
+	 * @return The workflow document.
+	 */
+	@GET
+	@Path(WF)
+	@Produces({ T2FLOW, SCUFL2, XML, JSON })
+	@Description("Gives the workflow document used to create the workflow run.")
+	@Nonnull
+	public Workflow getWorkflow();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(WF)
+	@Description("Produces the description of the run workflow.")
+	Response workflowOptions();
+
+	/** Get the workflow name. */
+	@GET
+	@Path(NAME)
+	@Produces(TEXT)
+	@Description("Gives the descriptive name of the workflow run.")
+	@Nonnull
+	public String getName();
+
+	/**
+	 * Set the workflow name.
+	 * 
+	 * @throws NoUpdateException
+	 *             If the user is not permitted to change the workflow.
+	 */
+	@PUT
+	@Path(NAME)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Set the descriptive name of the workflow run. Note that "
+			+ "this value may be arbitrarily truncated by the implementation.")
+	@Nonnull
+	public String setName(String name) throws NoUpdateException;
+
+	/** Produce the workflow name HTTP operations. */
+	@OPTIONS
+	@Path(NAME)
+	@Description("Produces the description of the operations on the run's "
+			+ "descriptive name.")
+	@Nonnull
+	Response nameOptions();
+
+	/**
+	 * Produces the name of the workflow's main profile.
+	 * 
+	 * @return The main profile name, or the empty string if there is no such
+	 *         profile.
+	 */
+	@GET
+	@Path(PROFILE)
+	@Produces(TEXT)
+	@Description("Gives the name of the workflow's main profile, or the empty string if none is defined.")
+	@Nonnull
+	String getMainProfileName();
+
+	/**
+	 * Get a description of the profiles supported by the workflow document used
+	 * to create this run.
+	 * 
+	 * @return A description of the supported profiles.
+	 */
+	@GET
+	@Path(PROFILE)
+	@Produces({ XML, JSON })
+	@Description("Describes what profiles exist on the workflow.")
+	@Nonnull
+	ProfileList getProfiles();
+
+	/** Produce the workflow profile HTTP operations. */
+	@OPTIONS
+	@Path(PROFILE)
+	@Description("Produces the description of the operations on the run's "
+			+ "profile.")
+	@Nonnull
+	Response profileOptions();
+
+	/**
+	 * Returns a resource that represents the workflow run's security
+	 * properties. These may only be accessed by the owner.
+	 * 
+	 * @return The security resource.
+	 * @throws NotOwnerException
+	 *             If the accessing principal isn't the owning principal.
+	 */
+	@Path(SEC)
+	@Description("Access the workflow run's security.")
+	@Nonnull
+	public TavernaServerSecurityREST getSecurity() throws NotOwnerException;
+
+	/**
+	 * Returns the time when the workflow run becomes eligible for automatic
+	 * deletion.
+	 * 
+	 * @return When the run expires.
+	 */
+	@GET
+	@Path(T_EXPIRE)
+	@Produces(TEXT)
+	@Description("Gives the time when the workflow run becomes eligible for "
+			+ "automatic deletion.")
+	@Nonnull
+	public String getExpiryTime();
+
+	/**
+	 * Sets the time when the workflow run becomes eligible for automatic
+	 * deletion.
+	 * 
+	 * @param expiry
+	 *            When the run will expire.
+	 * @return When the run will actually expire.
+	 * @throws NoUpdateException
+	 *             If the current user is not permitted to manage the lifetime
+	 *             of the run.
+	 */
+	@PUT
+	@Path(T_EXPIRE)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Sets the time when the workflow run becomes eligible for "
+			+ "automatic deletion.")
+	@Nonnull
+	public String setExpiryTime(@Nonnull String expiry)
+			throws NoUpdateException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(T_EXPIRE)
+	@Description("Produces the description of the run expiry.")
+	Response expiryOptions();
+
+	/**
+	 * Returns the time when the workflow run was created.
+	 * 
+	 * @return When the run was first submitted to the server.
+	 */
+	@GET
+	@Path(T_CREATE)
+	@Produces(TEXT)
+	@Description("Gives the time when the workflow run was first submitted "
+			+ "to the server.")
+	@Nonnull
+	public String getCreateTime();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(T_CREATE)
+	@Description("Produces the description of the run create time.")
+	Response createTimeOptions();
+
+	/**
+	 * Returns the time when the workflow run was started (through a user-driven
+	 * state change).
+	 * 
+	 * @return When the run was started, or <tt>null</tt>.
+	 */
+	@GET
+	@Path(T_START)
+	@Produces(TEXT)
+	@Description("Gives the time when the workflow run was started, or an "
+			+ "empty string if the run has not yet started.")
+	@Nonnull
+	public String getStartTime();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(T_START)
+	@Description("Produces the description of the run start time.")
+	Response startTimeOptions();
+
+	/**
+	 * Returns the time when the workflow run was detected to have finished.
+	 * 
+	 * @return When the run finished, or <tt>null</tt>.
+	 */
+	@GET
+	@Path(T_FINISH)
+	@Produces(TEXT)
+	@Description("Gives the time when the workflow run was first detected as "
+			+ "finished, or an empty string if it has not yet finished "
+			+ "(including if it has never started).")
+	@Nonnull
+	public String getFinishTime();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(T_FINISH)
+	@Description("Produces the description of the run finish time.")
+	Response finishTimeOptions();
+
+	/**
+	 * Gets the current status of the workflow run.
+	 * 
+	 * @return The status code.
+	 */
+	@GET
+	@Path(STATUS)
+	@Produces(TEXT)
+	@Description("Gives the current status of the workflow run.")
+	@Nonnull
+	public String getStatus();
+
+	/**
+	 * Sets the status of the workflow run. This does nothing if the status code
+	 * is the same as the run's current state.
+	 * 
+	 * @param status
+	 *            The new status code.
+	 * @return Description of what status the run is actually in, or a 202 to
+	 *         indicate that things are still changing.
+	 * @throws NoUpdateException
+	 *             If the current user is not permitted to update the run.
+	 * @throws BadStateChangeException
+	 *             If the state cannot be modified in the manner requested.
+	 */
+	@PUT
+	@Path(STATUS)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Attempts to update the status of the workflow run.")
+	@Nonnull
+	public Response setStatus(@Nonnull String status) throws NoUpdateException,
+			BadStateChangeException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(STATUS)
+	@Description("Produces the description of the run status.")
+	Response statusOptions();
+
+	/**
+	 * Get the working directory of this workflow run.
+	 * 
+	 * @return A RESTful delegate for the working directory.
+	 */
+	@Path(DIR)
+	@Description("Get the working directory of this workflow run.")
+	@Nonnull
+	public TavernaServerDirectoryREST getWorkingDirectory();
+
+	/**
+	 * Get the event listeners attached to this workflow run.
+	 * 
+	 * @return A RESTful delegate for the list of listeners.
+	 */
+	@Path(LISTEN)
+	@Description("Get the event listeners attached to this workflow run.")
+	@Nonnull
+	public TavernaServerListenersREST getListeners();
+
+	/**
+	 * Get a delegate for working with the inputs to this workflow run.
+	 * 
+	 * @param ui
+	 *            About the URI used to access this resource.
+	 * @return A RESTful delegate for the inputs.
+	 */
+	@Path(IN)
+	@Description("Get the inputs to this workflow run.")
+	@Nonnull
+	public TavernaServerInputREST getInputs(@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Get the output Baclava file for this workflow run.
+	 * 
+	 * @return The filename, or empty string to indicate that the outputs will
+	 *         be written to the <tt>out</tt> directory.
+	 */
+	@GET
+	@Path(OUT)
+	@Produces(TEXT)
+	@Description("Gives the Baclava file where output will be written; empty "
+			+ "means use multiple simple files in the out directory.")
+	@Nonnull
+	public String getOutputFile();
+
+	/**
+	 * Get a description of the outputs.
+	 * 
+	 * @param ui
+	 *            About the URI used to access this operation.
+	 * @return A description of the outputs (higher level than the filesystem).
+	 * @throws BadStateChangeException
+	 *             If the run is in the {@link Status#Initialized Initialized}
+	 *             state.
+	 * @throws FilesystemAccessException
+	 *             If problems occur when accessing the filesystem.
+	 * @throws NoDirectoryEntryException
+	 *             If things are odd in the filesystem.
+	 */
+	@GET
+	@Path(OUT)
+	@Produces({ XML, JSON })
+	@Description("Gives a description of the outputs, as currently understood")
+	@Nonnull
+	public OutputDescription getOutputDescription(@Nonnull @Context UriInfo ui)
+			throws BadStateChangeException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Set the output Baclava file for this workflow run.
+	 * 
+	 * @param filename
+	 *            The Baclava file to use, or empty to make the outputs be
+	 *            written to individual files in the <tt>out</tt> subdirectory
+	 *            of the working directory.
+	 * @return The Baclava file as actually set.
+	 * @throws NoUpdateException
+	 *             If the current user is not permitted to update the run.
+	 * @throws FilesystemAccessException
+	 *             If the filename is invalid (starts with <tt>/</tt> or
+	 *             contains a <tt>..</tt> segment).
+	 * @throws BadStateChangeException
+	 *             If the workflow is not in the Initialized state.
+	 */
+	@PUT
+	@Path(OUT)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Sets the Baclava file where output will be written; empty "
+			+ "means use multiple simple files in the out directory.")
+	@Nonnull
+	public String setOutputFile(@Nonnull String filename)
+			throws NoUpdateException, FilesystemAccessException,
+			BadStateChangeException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(OUT)
+	@Description("Produces the description of the run output.")
+	Response outputOptions();
+
+	/**
+	 * Get a handle to the interaction feed.
+	 * 
+	 * @return
+	 */
+	@Path(FEED_URL_DIR)
+	@Description("Access the interaction feed for the workflow run.")
+	@Nonnull
+	InteractionFeedREST getInteractionFeed();
+
+	/**
+	 * @return The stdout for the workflow run, or empty string if the run has
+	 *         not yet started.
+	 * @throws NoListenerException
+	 */
+	@GET
+	@Path(STDOUT)
+	@Description("Return the stdout for the workflow run.")
+	@Produces(TEXT)
+	@Nonnull
+	String getStdout() throws NoListenerException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(STDOUT)
+	@Description("Return the stdout for the workflow run.")
+	Response stdoutOptions();
+
+	/**
+	 * @return The stderr for the workflow run, or empty string if the run has
+	 *         not yet started.
+	 * @throws NoListenerException
+	 */
+	@GET
+	@Path(STDERR)
+	@Description("Return the stderr for the workflow run.")
+	@Produces(TEXT)
+	@Nonnull
+	String getStderr() throws NoListenerException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(STDERR)
+	@Description("Return the stderr for the workflow run.")
+	Response stderrOptions();
+
+	/**
+	 * @return The usage record for the workflow run, wrapped in a Response, or
+	 *         "empty content" if the run has not yet finished.
+	 * @throws NoListenerException
+	 * @throws JAXBException
+	 */
+	@GET
+	@Path(USAGE)
+	@Description("Return the usage record for the workflow run.")
+	@Produces(XML)
+	@Nonnull
+	Response getUsage() throws NoListenerException, JAXBException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(USAGE)
+	@Description("Return the usage record for the workflow run.")
+	Response usageOptions();
+
+	/**
+	 * @return The log for the workflow run, or empty string if the run has not
+	 *         yet started.
+	 */
+	@GET
+	@Path(LOG)
+	@Description("Return the log for the workflow run.")
+	@Produces(TEXT)
+	@Nonnull
+	Response getLogContents();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(LOG)
+	@Description("Return the log for the workflow run.")
+	Response logOptions();
+
+	/**
+	 * @return The log for the workflow run, or empty string if the run has not
+	 *         yet started.
+	 */
+	@GET
+	@Path(RUNBUNDLE)
+	@Description("Return the run bundle for the workflow run.")
+	@Produces(ROBUNDLE)
+	@Nonnull
+	Response getRunBundle();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(RUNBUNDLE)
+	@Description("Return the run bundle for the workflow run.")
+	Response runBundleOptions();
+
+	/**
+	 * @return Whether to create the run bundle for the workflow run. Only
+	 *         usefully set-able before the start of the run.
+	 */
+	@GET
+	@Path(GENERATE_PROVENANCE)
+	@Description("Whether to create the run bundle for the workflow run.")
+	@Produces(TEXT)
+	@Nonnull
+	boolean getGenerateProvenance();
+
+	/**
+	 * @param provenanceFlag
+	 *            Whether to create the run bundle for the workflow run. Only
+	 *            usefully set-able before the start of the run.
+	 * @return What it was actually set to.
+	 * @throws NoUpdateException 
+	 */
+	@PUT
+	@Path(GENERATE_PROVENANCE)
+	@Description("Whether to create the run bundle for the workflow run.")
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Nonnull
+	boolean setGenerateProvenance(boolean provenanceFlag) throws NoUpdateException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(GENERATE_PROVENANCE)
+	@Description("Whether to create the run bundle for the workflow run.")
+	Response generateProvenanceOptions();
+
+	/**
+	 * Factored out path names used in the {@link TavernaServerRunREST}
+	 * interface and related places.
+	 * 
+	 * @author Donal Fellows
+	 */
+	interface PathNames {
+		public static final String ROOT = "/";
+		public static final String WF = "workflow";
+		public static final String DIR = "wd";
+		public static final String NAME = "name";
+		public static final String T_EXPIRE = "expiry";
+		public static final String T_CREATE = "createTime";
+		public static final String T_START = "startTime";
+		public static final String T_FINISH = "finishTime";
+		public static final String STATUS = "status";
+		public static final String IN = "input";
+		public static final String OUT = "output";
+		public static final String PROFILE = "profile";
+		public static final String LISTEN = "listeners";
+		public static final String SEC = "security";
+		public static final String STDOUT = "stdout";
+		public static final String STDERR = "stderr";
+		public static final String USAGE = "usage";
+		public static final String LOG = "log";
+		public static final String RUNBUNDLE = "run-bundle";
+		public static final String GENERATE_PROVENANCE = "generate-provenance";
+	}
+
+	/**
+	 * The description of where everything is in a RESTful view of a workflow
+	 * run. Done with JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class RunDescription extends VersionedElement {
+		/** The identity of the owner of the workflow run. */
+		@XmlAttribute(namespace = Namespaces.SERVER_REST)
+		public String owner;
+		/** The description of the expiry. */
+		public Expiry expiry;
+		/** The location of the creation workflow description. */
+		public Uri creationWorkflow;
+		/** The location of the creation time property. */
+		public Uri createTime;
+		/** The location of the start time property. */
+		public Uri startTime;
+		/** The location of the finish time property. */
+		public Uri finishTime;
+		/** The location of the status description. */
+		public Uri status;
+		/** The location of the working directory. */
+		public Uri workingDirectory;
+		/** The location of the inputs. */
+		public Uri inputs;
+		/** The location of the Baclava output. */
+		public Uri output;
+		/** The location of the security context. */
+		public Uri securityContext;
+		/** The list of listeners. */
+		public ListenerList listeners;
+		/** The location of the interaction feed. */
+		public Uri interaction;
+		/** The name of the run. */
+		public Uri name;
+		/** The stdout of the run. */
+		public Uri stdout;
+		/** The stderr of the run. */
+		public Uri stderr;
+		/** The usage record for the run. */
+		public Uri usage;
+		/** The log from the run. */
+		public Uri log;
+		/** The bundle describing the run. */
+		@XmlElement(name = RUNBUNDLE)
+		public Uri runBundle;
+		/** Whether to generate a bundle describing the run. */
+		@XmlElement(name = GENERATE_PROVENANCE)
+		public Uri generateProvenance;
+
+		/**
+		 * How to describe a run's expiry.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "")
+		public static class Expiry {
+			/**
+			 * Where to go to read the exiry
+			 */
+			@XmlAttribute(name = "href", namespace = Namespaces.XLINK)
+			@XmlSchemaType(name = "anyURI")
+			public URI ref;
+			/**
+			 * What the expiry currently is.
+			 */
+			@XmlValue
+			public String timeOfDeath;
+
+			/**
+			 * Make a blank expiry description.
+			 */
+			public Expiry() {
+			}
+
+			private static DateTimeFormatter dtf;
+
+			Expiry(TavernaRun r, UriInfo ui, String path, String... parts) {
+				ref = fromUri(new Uri(ui, true, path, parts).ref).build();
+				if (dtf == null)
+					dtf = basicDateTime();
+				timeOfDeath = dtf.print(r.getExpiry().getTime());
+			}
+		}
+
+		/**
+		 * The description of a list of listeners attached to a run.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "")
+		public static class ListenerList extends Uri {
+			/**
+			 * The references to the individual listeners.
+			 */
+			public List<Uri> listener;
+
+			/**
+			 * An empty description of listeners.
+			 */
+			public ListenerList() {
+				listener = new ArrayList<>();
+			}
+
+			/**
+			 * @param r
+			 *            The run whose listeners we're talking about.
+			 * @param ub
+			 *            Uri factory; must've been secured
+			 */
+			private ListenerList(TavernaRun r, UriBuilder ub) {
+				super(ub);
+				listener = new ArrayList<>(r.getListeners().size());
+				UriBuilder pathUB = ub.clone().path("{name}");
+				for (Listener l : r.getListeners())
+					listener.add(new Uri(pathUB.build(l.getName())));
+			}
+
+			/**
+			 * @param run
+			 *            The run whose listeners we're talking about.
+			 * @param ui
+			 *            The source of information about URIs.
+			 * @param path
+			 *            Where we are relative to the URI source.
+			 * @param parts
+			 *            Anything required to fill out the path.
+			 */
+			ListenerList(TavernaRun run, UriInfo ui, String path,
+					String... parts) {
+				this(run, secure(fromUri(new Uri(ui, path, parts).ref)));
+			}
+		}
+
+		/**
+		 * An empty description of a run.
+		 */
+		public RunDescription() {
+		}
+
+		/**
+		 * A description of a particular run.
+		 * 
+		 * @param run
+		 *            The run to describe.
+		 * @param ui
+		 *            The factory for URIs.
+		 */
+		public RunDescription(TavernaRun run, UriInfo ui) {
+			super(true);
+			creationWorkflow = new Uri(ui, WF);
+			expiry = new Expiry(run, ui, T_EXPIRE);
+			status = new Uri(ui, STATUS);
+			workingDirectory = new Uri(ui, DIR);
+			listeners = new ListenerList(run, ui, LISTEN);
+			securityContext = new Uri(ui, SEC);
+			inputs = new Uri(ui, IN);
+			output = new Uri(ui, OUT);
+			createTime = new Uri(ui, T_CREATE);
+			startTime = new Uri(ui, T_START);
+			finishTime = new Uri(ui, T_FINISH);
+			interaction = new Uri(ui, FEED_URL_DIR);
+			name = new Uri(ui, NAME);
+			owner = run.getSecurityContext().getOwner().getName();
+			stdout = new Uri(ui, STDOUT);
+			stderr = new Uri(ui, STDERR);
+			usage = new Uri(ui, USAGE);
+			log = new Uri(ui, LOG);
+			runBundle = new Uri(ui, RUNBUNDLE);
+			generateProvenance = new Uri(ui, GENERATE_PROVENANCE);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/TavernaServerSecurityREST.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/TavernaServerSecurityREST.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/TavernaServerSecurityREST.java
new file mode 100644
index 0000000..f5101e7
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/TavernaServerSecurityREST.java
@@ -0,0 +1,788 @@
+/*
+ */
+package org.taverna.server.master.rest;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static java.util.Collections.emptyList;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.TEXT;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.CREDS;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.ONE_CRED;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.ONE_PERM;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.ONE_TRUST;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.OWNER;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.PERMS;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.ROOT;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.TRUSTS;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElements;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.Permission;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.exceptions.NoCredentialException;
+
+/**
+ * Manages the security of the workflow run. In general, only the owner of a run
+ * may access this resource. Many of these security-related resources may only
+ * be changed before the run is set to operating.
+ * 
+ * @author Donal Fellows
+ */
+@RolesAllowed(USER)
+@Description("Manages the security of the workflow run. In general, only the "
+		+ "owner of a run may access this resource.")
+public interface TavernaServerSecurityREST {
+	interface PathNames {
+		final String ROOT = "/";
+		final String OWNER = "owner";
+		final String CREDS = "credentials";
+		final String ONE_CRED = CREDS + "/{id}";
+		final String TRUSTS = "trusts";
+		final String ONE_TRUST = TRUSTS + "/{id}";
+		final String PERMS = "permissions";
+		final String ONE_PERM = PERMS + "/{id}";
+	}
+
+	/**
+	 * Gets a description of the security information supported by the workflow
+	 * run.
+	 * 
+	 * @param ui
+	 *            About the URI used to access this resource.
+	 * @return A description of the security information.
+	 */
+	@GET
+	@Path(ROOT)
+	@Produces({ XML, JSON })
+	@Description("Gives a description of the security information supported "
+			+ "by the workflow run.")
+	@Nonnull
+	Descriptor describe(@Nonnull @Context UriInfo ui);
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ROOT)
+	@Description("Produces the description of the run security.")
+	Response descriptionOptions();
+
+	/**
+	 * Gets the identity of who owns the workflow run.
+	 * 
+	 * @return The name of the owner of the run.
+	 */
+	@GET
+	@Path(OWNER)
+	@Produces(TEXT)
+	@Description("Gives the identity of who owns the workflow run.")
+	@Nonnull
+	String getOwner();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(OWNER)
+	@Description("Produces the description of the run owner.")
+	Response ownerOptions();
+
+	/*
+	 * @PUT @Path("/") @Consumes(ContentTypes.BYTES) @CallCounted @Nonnull
+	 * public void set(@Nonnull InputStream contents, @Nonnull @Context UriInfo
+	 * ui);
+	 */
+
+	/**
+	 * @return A list of credentials supplied to this workflow run.
+	 */
+	@GET
+	@Path(CREDS)
+	@Produces({ XML, JSON })
+	@Description("Gives a list of credentials supplied to this workflow run.")
+	@Nonnull
+	CredentialList listCredentials();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(CREDS)
+	@Description("Produces the description of the run credentials' operations.")
+	Response credentialsOptions();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ONE_CRED)
+	@Description("Produces the description of one run credential's operations.")
+	Response credentialOptions(@PathParam("id") String id);
+
+	/**
+	 * Describe a particular credential.
+	 * 
+	 * @param id
+	 *            The id of the credential to fetch.
+	 * @return The description of the credential.
+	 * @throws NoCredentialException
+	 *             If the credential doesn't exist.
+	 */
+	@GET
+	@Path(ONE_CRED)
+	@Produces({ XML, JSON })
+	@Description("Describes a particular credential.")
+	@Nonnull
+	CredentialHolder getParticularCredential(@Nonnull @PathParam("id") String id)
+			throws NoCredentialException;
+
+	/**
+	 * Update a particular credential.
+	 * 
+	 * @param id
+	 *            The id of the credential to update.
+	 * @param c
+	 *            The details of the credential to use in the update.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return Description of the updated credential.
+	 * @throws InvalidCredentialException
+	 *             If the credential description isn't valid.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@PUT
+	@Path(ONE_CRED)
+	@Consumes({ XML, JSON })
+	@Produces({ XML, JSON })
+	@Description("Updates a particular credential.")
+	@Nonnull
+	CredentialHolder setParticularCredential(
+			@Nonnull @PathParam("id") String id, @Nonnull CredentialHolder c,
+			@Nonnull @Context UriInfo ui) throws InvalidCredentialException,
+			BadStateChangeException;
+
+	/**
+	 * Adds a new credential.
+	 * 
+	 * @param c
+	 *            The details of the credential to create.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return Description of the created credential.
+	 * @throws InvalidCredentialException
+	 *             If the credential description isn't valid.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@POST
+	@Path(CREDS)
+	@Consumes({ XML, JSON })
+	@Description("Creates a new credential.")
+	@Nonnull
+	Response addCredential(@Nonnull CredentialHolder c,
+			@Nonnull @Context UriInfo ui) throws InvalidCredentialException,
+			BadStateChangeException;
+
+	/**
+	 * Deletes all credentials associated with a run.
+	 * 
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return A characterisation of a successful delete.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@DELETE
+	@Path(CREDS)
+	@Description("Deletes all credentials.")
+	@Nonnull
+	Response deleteAllCredentials(@Nonnull @Context UriInfo ui)
+			throws BadStateChangeException;
+
+	/**
+	 * Deletes one credential associated with a run.
+	 * 
+	 * @param id
+	 *            The identity of the credential to delete.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return A characterisation of a successful delete.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@DELETE
+	@Path(ONE_CRED)
+	@Description("Deletes a particular credential.")
+	@Nonnull
+	Response deleteCredential(@Nonnull @PathParam("id") String id,
+			@Nonnull @Context UriInfo ui) throws BadStateChangeException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(TRUSTS)
+	@Description("Produces the description of the run trusted certificates' "
+			+ "operations.")
+	Response trustsOptions();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ONE_TRUST)
+	@Description("Produces the description of one run trusted certificate's "
+			+ "operations.")
+	Response trustOptions(@PathParam("id") String id);
+
+	/**
+	 * @return A list of trusted identities supplied to this workflow run.
+	 */
+	@GET
+	@Path(TRUSTS)
+	@Produces({ XML, JSON })
+	@Description("Gives a list of trusted identities supplied to this "
+			+ "workflow run.")
+	@Nonnull
+	TrustList listTrusted();
+
+	/**
+	 * Describe a particular trusted identity.
+	 * 
+	 * @param id
+	 *            The id of the trusted identity to fetch.
+	 * @return The description of the trusted identity.
+	 * @throws NoCredentialException
+	 *             If the trusted identity doesn't exist.
+	 */
+	@GET
+	@Path(ONE_TRUST)
+	@Produces({ XML, JSON })
+	@Description("Describes a particular trusted identity.")
+	@Nonnull
+	Trust getParticularTrust(@Nonnull @PathParam("id") String id)
+			throws NoCredentialException;
+
+	/**
+	 * Update a particular trusted identity.
+	 * 
+	 * @param id
+	 *            The id of the trusted identity to update.
+	 * @param t
+	 *            The details of the trusted identity to use in the update.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return Description of the updated trusted identity.
+	 * @throws InvalidCredentialException
+	 *             If the trusted identity description isn't valid.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@PUT
+	@Path(ONE_TRUST)
+	@Consumes({ XML, JSON })
+	@Produces({ XML, JSON })
+	@Description("Updates a particular trusted identity.")
+	@Nonnull
+	Trust setParticularTrust(@Nonnull @PathParam("id") String id,
+			@Nonnull Trust t, @Nonnull @Context UriInfo ui)
+			throws InvalidCredentialException, BadStateChangeException;
+
+	/**
+	 * Adds a new trusted identity.
+	 * 
+	 * @param t
+	 *            The details of the trusted identity to create.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return Description of the created trusted identity.
+	 * @throws InvalidCredentialException
+	 *             If the trusted identity description isn't valid.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@POST
+	@Path(TRUSTS)
+	@Consumes({ XML, JSON })
+	@Description("Adds a new trusted identity.")
+	@Nonnull
+	Response addTrust(@Nonnull Trust t, @Nonnull @Context UriInfo ui)
+			throws InvalidCredentialException, BadStateChangeException;
+
+	/**
+	 * Deletes all trusted identities associated with a run.
+	 * 
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return A characterisation of a successful delete.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@DELETE
+	@Path(TRUSTS)
+	@Description("Deletes all trusted identities.")
+	@Nonnull
+	Response deleteAllTrusts(@Nonnull @Context UriInfo ui)
+			throws BadStateChangeException;
+
+	/**
+	 * Deletes one trusted identity associated with a run.
+	 * 
+	 * @param id
+	 *            The identity of the trusted identity to delete.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return A characterisation of a successful delete.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@DELETE
+	@Path(ONE_TRUST)
+	@Description("Deletes a particular trusted identity.")
+	@Nonnull
+	Response deleteTrust(@Nonnull @PathParam("id") String id,
+			@Nonnull @Context UriInfo ui) throws BadStateChangeException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(PERMS)
+	@Description("Produces the description of the run permissions' operations.")
+	Response permissionsOptions();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ONE_PERM)
+	@Description("Produces the description of one run permission's operations.")
+	Response permissionOptions(@PathParam("id") String id);
+
+	/**
+	 * @return A list of (non-default) permissions associated with this workflow
+	 *         run.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 */
+	@GET
+	@Path(PERMS)
+	@Produces({ XML, JSON })
+	@Description("Gives a list of all non-default permissions associated with "
+			+ "the enclosing workflow run. By default, nobody has any access "
+			+ "at all except for the owner of the run.")
+	@Nonnull
+	PermissionsDescription describePermissions(@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Describe the particular permission granted to a user.
+	 * 
+	 * @param id
+	 *            The name of the user whose permissions are to be described.
+	 * @return The permission they are granted.
+	 */
+	@GET
+	@Path(ONE_PERM)
+	@Produces(TEXT)
+	@Description("Describes the permission granted to a particular user.")
+	@Nonnull
+	Permission describePermission(@Nonnull @PathParam("id") String id);
+
+	/**
+	 * Update the permission granted to a user.
+	 * 
+	 * @param id
+	 *            The name of the user whose permissions are to be updated. Note
+	 *            that the owner always has full permissions.
+	 * @param perm
+	 *            The permission level to set.
+	 * @return The permission level that has actually been set.
+	 */
+	@PUT
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Path(ONE_PERM)
+	@Description("Updates the permissions granted to a particular user.")
+	@Nonnull
+	Permission setPermission(@Nonnull @PathParam("id") String id,
+			@Nonnull Permission perm);
+
+	/**
+	 * Delete the permissions associated with a user, which restores them to the
+	 * default (no access unless they are the owner or have admin privileges).
+	 * 
+	 * @param id
+	 *            The name of the user whose permissions are to be revoked.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return An indication that the delete has been successful (or not).
+	 */
+	@DELETE
+	@Path(ONE_PERM)
+	@Description("Deletes (by resetting to default) the permissions "
+			+ "associated with a particular user.")
+	@Nonnull
+	Response deletePermission(@Nonnull @PathParam("id") String id,
+			@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Manufacture a permission setting for a previously-unknown user.
+	 * 
+	 * @param desc
+	 *            A description of the name of the user and the permission level
+	 *            to grant them.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return An indication that the create has been successful (or not).
+	 */
+	@POST
+	@Path(PERMS)
+	@Consumes({ XML, JSON })
+	@Description("Creates a new assignment of permissions to a particular user.")
+	@Nonnull
+	Response makePermission(@Nonnull PermissionDescription desc,
+			@Nonnull @Context UriInfo ui);
+
+	/**
+	 * A description of the security resources associated with a workflow run.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "securityDescriptor")
+	@XmlType(name = "SecurityDescriptor")
+	public static final class Descriptor extends VersionedElement {
+		/** The identity of the owner of the enclosing workflow run. */
+		@XmlElement
+		public String owner;
+		/** Where to get the permissions on the run. */
+		@XmlElement
+		public Uri permissions;
+
+		/** Characterisation of the credentials attached to the run. */
+		@XmlElement
+		public Credentials credentials;
+		/** Characterisation of the trusted certificates attached to the run. */
+		@XmlElement
+		public Trusts trusts;
+
+		public Descriptor() {
+		}
+
+		/**
+		 * Initialise a description of the security context.
+		 * 
+		 * @param ub
+		 *            How to build URIs.
+		 * @param owner
+		 *            Who owns the context.
+		 * @param credential
+		 *            The credentials associated with the context.
+		 * @param trust
+		 *            The trusted certificates associated with the context.
+		 */
+		public Descriptor(@Nonnull UriBuilder ub, @Nonnull String owner,
+				@Nonnull Credential[] credential, @Nonnull Trust[] trust) {
+			super(true);
+			this.owner = owner;
+			this.permissions = new Uri(ub, PERMS);
+			this.credentials = new Credentials(new Uri(ub, CREDS).ref,
+					credential);
+			this.trusts = new Trusts(new Uri(ub, TRUSTS).ref, trust);
+		}
+
+		/**
+		 * A description of credentials associated with a workflow run.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "CredentialCollection")
+		public static final class Credentials {
+			/** Reference to the collection of credentials */
+			@XmlAttribute(name = "href", namespace = XLINK)
+			@XmlSchemaType(name = "anyURI")
+			public URI href;
+			/** Descriptions of the credentials themselves. */
+			@XmlElement
+			public List<CredentialHolder> credential = new ArrayList<>();
+
+			public Credentials() {
+			}
+
+			/**
+			 * Initialise a description of the credentials.
+			 * 
+			 * @param uri
+			 *            the URI of the collection.
+			 * @param credential
+			 *            The credentials in the collection.
+			 */
+			public Credentials(@Nonnull URI uri,
+					@Nonnull Credential[] credential) {
+				this.href = uri;
+				for (Credential c : credential)
+					this.credential.add(new CredentialHolder(c));
+			}
+		}
+
+		/**
+		 * A description of trusted certificates associated with a workflow run.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "TrustCollection")
+		public static final class Trusts {
+			/** Reference to the collection of trusted certs */
+			@XmlAttribute(name = "href", namespace = XLINK)
+			@XmlSchemaType(name = "anyURI")
+			public URI href;
+			/** Descriptions of the trusted certs themselves. */
+			@XmlElement
+			public Trust[] trust;
+
+			public Trusts() {
+			}
+
+			/**
+			 * Initialise a description of the trusted certificates.
+			 * 
+			 * @param uri
+			 *            the URI of the collection.
+			 * @param trust
+			 *            The trusted certificates in the collection.
+			 */
+			public Trusts(@Nonnull URI uri, @Nonnull Trust[] trust) {
+				this.href = uri;
+				this.trust = trust.clone();
+			}
+		}
+	}
+
+	/**
+	 * A container for a credential, used to work around issues with type
+	 * inference in CXF's REST service handling and JAXB.
+	 * 
+	 * @see Credential.KeyPair
+	 * @see Credential.Password
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "credential")
+	@XmlType(name = "Credential")
+	public static final class CredentialHolder {
+		/**
+		 * The credential inside this holder.
+		 */
+		@XmlElements({
+				@XmlElement(name = "keypair", namespace = SERVER, type = Credential.KeyPair.class, required = true),
+				@XmlElement(name = "userpass", namespace = SERVER, type = Credential.Password.class, required = true) })
+		public Credential credential;
+
+		public CredentialHolder() {
+		}
+
+		public CredentialHolder(Credential credential) {
+			this.credential = credential;
+		}
+
+		/**
+		 * Convenience accessor function.
+		 * 
+		 * @return The keypair credential held in this holder.
+		 */
+		@XmlTransient
+		public Credential.KeyPair getKeypair() {
+			return (Credential.KeyPair) this.credential;
+		}
+
+		/**
+		 * Convenience accessor function.
+		 * 
+		 * @return The userpass credential held in this holder.
+		 */
+		@XmlTransient
+		public Credential.Password getUserpass() {
+			return (Credential.Password) this.credential;
+		}
+	}
+
+	/**
+	 * A simple list of credential descriptions.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "credentials")
+	public static final class CredentialList extends VersionedElement {
+		/** The descriptions of the credentials */
+		@XmlElement
+		@Nonnull
+		public List<CredentialHolder> credential = new ArrayList<>();
+
+		public CredentialList() {
+		}
+
+		/**
+		 * Initialise the list of credentials.
+		 * 
+		 * @param credential
+		 *            The descriptions of individual credentials.
+		 */
+		public CredentialList(@Nonnull Credential[] credential) {
+			super(true);
+			for (Credential c : credential)
+				this.credential.add(new CredentialHolder(c));
+		}
+	}
+
+	/**
+	 * A simple list of trusted certificate descriptions.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "trustedIdentities")
+	public static final class TrustList extends VersionedElement {
+		/** The descriptions of the trusted certificates */
+		@XmlElement
+		public Trust[] trust;
+
+		public TrustList() {
+		}
+
+		/**
+		 * Initialise the list of trusted certificates.
+		 * 
+		 * @param trust
+		 *            The descriptions of individual certificates.
+		 */
+		public TrustList(@Nonnull Trust[] trust) {
+			super(true);
+			this.trust = trust.clone();
+		}
+	}
+
+	/**
+	 * A description of the permissions granted to others by the owner of a
+	 * workflow run.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "permissionsDescriptor")
+	public static class PermissionsDescription extends VersionedElement {
+		/**
+		 * A description of the permissions granted to one user by the owner of
+		 * a workflow run.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlRootElement(name = "userPermission")
+		public static class LinkedPermissionDescription extends Uri {
+			/** Who is this granted to? */
+			@XmlElement
+			public String userName;
+			/** What are they granted? */
+			@XmlElement
+			public Permission permission;
+
+			public LinkedPermissionDescription() {
+			}
+
+			/**
+			 * Initialise a description of one user's permissions.
+			 * 
+			 * @param ub
+			 *            How to build the URI to this permission. Already
+			 *            secured.
+			 * @param userName
+			 *            Who this relates to.
+			 * @param permission
+			 *            What permission is granted.
+			 * @param strings
+			 *            Parameters to the URI builder.
+			 */
+			LinkedPermissionDescription(@Nonnull UriBuilder ub,
+					@Nonnull String userName, @Nonnull Permission permission,
+					String... strings) {
+				super(ub, strings);
+				this.userName = userName;
+				this.permission = permission;
+			}
+		}
+
+		/** List of descriptions of permissions. */
+		@XmlElement
+		public List<LinkedPermissionDescription> permission;
+
+		public PermissionsDescription() {
+			permission = emptyList();
+		}
+
+		/**
+		 * Initialise the description of a collection of permissions.
+		 * 
+		 * @param ub
+		 *            How to build URIs to this collection. Must have already
+		 *            been secured.
+		 * @param permissionMap
+		 *            The permissions to describe.
+		 */
+		public PermissionsDescription(@Nonnull UriBuilder ub,
+				@Nonnull Map<String, Permission> permissionMap) {
+			permission = new ArrayList<>();
+			List<String> userNames = new ArrayList<>(permissionMap.keySet());
+			Collections.sort(userNames);
+			for (String user : userNames)
+				permission.add(new LinkedPermissionDescription(ub, user,
+						permissionMap.get(user), user));
+		}
+	}
+
+	/**
+	 * An instruction to update the permissions for a user.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "permissionUpdate")
+	public static class PermissionDescription {
+		/** Who to set the permission for? */
+		@XmlElement
+		public String userName;
+		/** What permission to grant them? */
+		@XmlElement
+		public Permission permission;
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/AccessDeniedHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/AccessDeniedHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/AccessDeniedHandler.java
new file mode 100644
index 0000000..3418975
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/AccessDeniedHandler.java
@@ -0,0 +1,34 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+import org.springframework.security.access.AccessDeniedException;
+
+public class AccessDeniedHandler extends HandlerCore implements
+		ExceptionMapper<AccessDeniedException> {
+	@Override
+	public Response toResponse(AccessDeniedException exception) {
+		return respond(FORBIDDEN, exception);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadInputPortNameHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadInputPortNameHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadInputPortNameHandler.java
new file mode 100644
index 0000000..a78693d
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadInputPortNameHandler.java
@@ -0,0 +1,36 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.BadInputPortNameException;
+
+@Provider
+public class BadInputPortNameHandler extends HandlerCore implements
+		ExceptionMapper<BadInputPortNameException> {
+	@Override
+	public Response toResponse(BadInputPortNameException exn) {
+		return respond(NOT_FOUND, exn);
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadPropertyValueHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadPropertyValueHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadPropertyValueHandler.java
new file mode 100644
index 0000000..e956749
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadPropertyValueHandler.java
@@ -0,0 +1,36 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+
+@Provider
+public class BadPropertyValueHandler extends HandlerCore implements
+		ExceptionMapper<BadPropertyValueException> {
+	@Override
+	public Response toResponse(BadPropertyValueException exn) {
+		return respond(BAD_REQUEST, exn);
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadStateChangeHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadStateChangeHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadStateChangeHandler.java
new file mode 100644
index 0000000..53f441b
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/BadStateChangeHandler.java
@@ -0,0 +1,36 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.BadStateChangeException;
+
+@Provider
+public class BadStateChangeHandler extends HandlerCore implements
+		ExceptionMapper<BadStateChangeException> {
+	@Override
+	public Response toResponse(BadStateChangeException exn) {
+		return respond(BAD_REQUEST, exn);
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/EntryHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/EntryHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/EntryHandler.java
new file mode 100644
index 0000000..bc79c22
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/EntryHandler.java
@@ -0,0 +1,147 @@
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonMap;
+import static javax.ws.rs.core.Response.notAcceptable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Variant;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.model.Document;
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.parser.Parser;
+import org.apache.abdera.writer.Writer;
+import org.springframework.beans.factory.annotation.Required;
+
+@Provider
+@Produces({ "application/atom+xml", "application/atom+xml;type=entry" })
+@Consumes({ "application/atom+xml", "application/atom+xml;type=entry" })
+public class EntryHandler implements MessageBodyWriter<Entry>,
+		MessageBodyReader<Entry> {
+	private static final String ENC = "UTF-8";
+	private static final MediaType ENTRY = new MediaType("application",
+			"atom+xml", singletonMap("type", "entry"));
+	private static final Variant VARIANT = new Variant(ENTRY, (String) null,
+			ENC);
+	private static final Charset UTF8 = Charset.forName(ENC);
+
+	@Required
+	public void setAbdera(Abdera abdera) {
+		parser = abdera.getParser();
+		writer = abdera.getWriterFactory().getWriter("prettyxml");
+	}
+
+	private Parser parser;
+	private Writer writer;
+
+	@Override
+	public boolean isReadable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (!Entry.class.isAssignableFrom(type))
+			return false;
+		if (!ENTRY.isCompatible(mediaType))
+			return false;
+		if (mediaType.getParameters().containsKey("type"))
+			return "entry".equalsIgnoreCase(mediaType.getParameters().get(
+					"type"));
+		return true;
+	}
+
+	@Override
+	public Entry readFrom(Class<Entry> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+			throws IOException, WebApplicationException {
+		Charset cs = UTF8;
+		try {
+			String charset = mediaType.getParameters().get("charset");
+			if (charset != null)
+				cs = Charset.forName(charset);
+		} catch (IllegalCharsetNameException e) {
+			throw new WebApplicationException(notAcceptable(asList(VARIANT))
+					.entity("bad charset name").build());
+		} catch (UnsupportedCharsetException e) {
+			throw new WebApplicationException(notAcceptable(asList(VARIANT))
+					.entity("unsupportd charset name").build());
+		}
+		try {
+			Document<Entry> doc = parser.parse(new InputStreamReader(
+					entityStream, cs));
+			if (!Entry.class.isAssignableFrom(doc.getRoot().getClass())) {
+				throw new WebApplicationException(
+						notAcceptable(asList(VARIANT)).entity(
+								"not really a feed entry").build());
+			}
+			return doc.getRoot();
+		} catch (ClassCastException e) {
+			throw new WebApplicationException(notAcceptable(asList(VARIANT))
+					.entity("not really a feed entry").build());
+
+		}
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (!Entry.class.isAssignableFrom(type))
+			return false;
+		if (!ENTRY.isCompatible(mediaType))
+			return false;
+		if (mediaType.getParameters().containsKey("type"))
+			return "entry".equalsIgnoreCase(mediaType.getParameters().get(
+					"type"));
+		return true;
+	}
+
+	@Override
+	public long getSize(Entry t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return -1;
+	}
+
+	@Override
+	public void writeTo(Entry t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		httpHeaders.putSingle("Content-Type", ENTRY.toString() + ";charset="
+				+ ENC);
+		writer.writeTo(t, new OutputStreamWriter(entityStream, UTF8));
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FeedHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FeedHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FeedHandler.java
new file mode 100644
index 0000000..77e7e49
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FeedHandler.java
@@ -0,0 +1,82 @@
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static java.util.Collections.singletonMap;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.model.Feed;
+import org.apache.abdera.writer.Writer;
+import org.springframework.beans.factory.annotation.Required;
+
+@Provider
+@Produces({ "application/atom+xml", "application/atom+xml;type=feed" })
+public class FeedHandler implements MessageBodyWriter<Feed> {
+	private static final MediaType FEED = new MediaType("application",
+			"atom+xml", singletonMap("type", "feed"));
+	private static final String ENC = "UTF-8";
+
+	@Required
+	public void setAbdera(Abdera abdera) {
+		writer = abdera.getWriterFactory().getWriter("prettyxml");
+	}
+
+	private Writer writer;
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (!Feed.class.isAssignableFrom(type))
+			return false;
+		if (!FEED.isCompatible(mediaType))
+			return false;
+		if (mediaType.getParameters().containsKey("type"))
+			return "feed".equalsIgnoreCase(mediaType.getParameters()
+					.get("type"));
+		return true;
+	}
+
+	@Override
+	public long getSize(Feed t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return -1;
+	}
+
+	@Override
+	public void writeTo(Feed t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		httpHeaders.putSingle("Content-Type", FEED.toString() + ";charset="
+				+ ENC);
+		writer.writeTo(t, new OutputStreamWriter(entityStream, ENC));
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileConcatenationHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileConcatenationHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileConcatenationHandler.java
new file mode 100644
index 0000000..e0924ad
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileConcatenationHandler.java
@@ -0,0 +1,77 @@
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.FileConcatenation;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+public class FileConcatenationHandler implements
+		MessageBodyWriter<FileConcatenation> {
+	/** How much to pull from the worker in one read. */
+	private int maxChunkSize;
+
+	/**
+	 * @param maxChunkSize
+	 *            How much to pull from the worker in one read.
+	 */
+	@Required
+	public void setMaxChunkSize(int maxChunkSize) {
+		this.maxChunkSize = maxChunkSize;
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return type.isAssignableFrom(FileConcatenation.class);
+	}
+
+	@Override
+	public long getSize(FileConcatenation fc, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return fc.size();
+	}
+
+	@Override
+	public void writeTo(FileConcatenation fc, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException {
+		for (File f : fc)
+			try {
+				byte[] buffer;
+				for (int off = 0; true ; off += buffer.length) {
+					buffer = f.getContents(off, maxChunkSize);
+					if (buffer == null || buffer.length == 0)
+						break;
+					entityStream.write(buffer);
+				}
+			} catch (FilesystemAccessException e) {
+				// Ignore/skip to next file
+			}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileMessageHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileMessageHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileMessageHandler.java
new file mode 100644
index 0000000..7d2b381
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileMessageHandler.java
@@ -0,0 +1,93 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.commons.logging.Log;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+/**
+ * How to write out a File object with JAX-RS.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+public class FileMessageHandler implements MessageBodyWriter<File> {
+	private Log log = getLog("Taverna.Server.Webapp");
+	/** How much to pull from the worker in one read. */
+	private int maxChunkSize;
+
+	/**
+	 * @param maxChunkSize
+	 *            How much to pull from the worker in one read.
+	 */
+	public void setMaxChunkSize(int maxChunkSize) {
+		this.maxChunkSize = maxChunkSize;
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return File.class.isAssignableFrom(type);
+	}
+
+	@Override
+	public long getSize(File t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		try {
+			return t.getSize(); // Is it really raw bytes?
+		} catch (FilesystemAccessException e) {
+			log.info("failed to get file length", e);
+			return -1;
+		}
+	}
+
+	@Override
+	public void writeTo(File t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		try {
+			int off = 0;
+			while (true) {
+				byte[] buffer = t.getContents(off, maxChunkSize);
+				if (buffer == null || buffer.length == 0)
+					break;
+				entityStream.write(buffer);
+				off += buffer.length;
+			}
+		} catch (FilesystemAccessException e) {
+			throw new IOException("problem when reading file", e);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileSegmentHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileSegmentHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileSegmentHandler.java
new file mode 100644
index 0000000..82d5e0a
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FileSegmentHandler.java
@@ -0,0 +1,87 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static java.lang.Math.min;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.rest.FileSegment;
+
+/**
+ * How to write out a segment of a file with JAX-RS.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+public class FileSegmentHandler implements MessageBodyWriter<FileSegment> {
+	/** How much to pull from the worker in one read. */
+	private int maxChunkSize;
+
+	/**
+	 * @param maxChunkSize
+	 *            How much to pull from the worker in one read.
+	 */
+	public void setMaxChunkSize(int maxChunkSize) {
+		this.maxChunkSize = maxChunkSize;
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return FileSegment.class.isAssignableFrom(type);
+	}
+
+	@Override
+	public long getSize(FileSegment t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return t.to - t.from;
+	}
+
+	@Override
+	public void writeTo(FileSegment t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		try {
+			int off = t.from;
+			while (off < t.to) {
+				byte[] buffer = t.file.getContents(off,
+						min(maxChunkSize, t.to - off));
+				if (buffer == null || buffer.length == 0)
+					break;
+				entityStream.write(buffer);
+				off += buffer.length;
+			}
+		} catch (FilesystemAccessException e) {
+			throw new IOException("problem when reading file", e);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FilesystemAccessHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FilesystemAccessHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FilesystemAccessHandler.java
new file mode 100644
index 0000000..cfa863c
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/FilesystemAccessHandler.java
@@ -0,0 +1,36 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+@Provider
+public class FilesystemAccessHandler extends HandlerCore implements
+		ExceptionMapper<FilesystemAccessException> {
+	@Override
+	public Response toResponse(FilesystemAccessException exn) {
+		return respond(FORBIDDEN, exn);
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/GeneralFailureHandler.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/GeneralFailureHandler.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/GeneralFailureHandler.java
new file mode 100644
index 0000000..fe4ba0b
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/GeneralFailureHandler.java
@@ -0,0 +1,34 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+import org.taverna.server.master.exceptions.GeneralFailureException;
+
+public class GeneralFailureHandler extends HandlerCore implements
+		ExceptionMapper<GeneralFailureException> {
+	@Override
+	public Response toResponse(GeneralFailureException exception) {
+		return respond(INTERNAL_SERVER_ERROR, exception);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/HandlerCore.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/HandlerCore.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/HandlerCore.java
new file mode 100644
index 0000000..bc92154
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/rest/handler/HandlerCore.java
@@ -0,0 +1,84 @@
+/*
+ */
+package org.taverna.server.master.rest.handler;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
+import static javax.ws.rs.core.Response.status;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.commons.logging.Log;
+import org.taverna.server.master.api.ManagementModel;
+
+/**
+ * Base class for handlers that grants Spring-enabled access to the management
+ * model.
+ * 
+ * @author Donal Fellows
+ */
+public class HandlerCore {
+	private Log log = getLog("Taverna.Server.Webapp");
+	private ManagementModel managementModel;
+
+	/**
+	 * @param managementModel
+	 *            the managementModel to set
+	 */
+	public void setManagementModel(ManagementModel managementModel) {
+		this.managementModel = managementModel;
+	}
+
+	/**
+	 * Simplified interface for building responses.
+	 * 
+	 * @param status
+	 *            What status code to use?
+	 * @param exception
+	 *            What exception to report on?
+	 * @return The build response.
+	 */
+	protected Response respond(Response.Status status, Exception exception) {
+		if (managementModel.getLogOutgoingExceptions()
+				|| status.getStatusCode() >= 500)
+			log.info("converting exception to response", exception);
+		return status(status).type(TEXT_PLAIN_TYPE)
+				.entity(exception.getMessage()).build();
+	}
+
+	/**
+	 * Simplified interface for building responses.
+	 * 
+	 * @param status
+	 *            What status code to use?
+	 * @param partialMessage
+	 *            The prefix to the message.
+	 * @param exception
+	 *            What exception to report on?
+	 * @return The build response.
+	 */
+	protected Response respond(Response.Status status, String partialMessage,
+			Exception exception) {
+		if (managementModel.getLogOutgoingExceptions()
+				|| status.getStatusCode() >= 500)
+			log.info("converting exception to response", exception);
+		return status(status).type(TEXT_PLAIN_TYPE)
+				.entity(partialMessage + "\n" + exception.getMessage()).build();
+	}
+}