http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/DirectoryREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/DirectoryREST.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/DirectoryREST.java
new file mode 100644
index 0000000..48969fa
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/DirectoryREST.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.ok;
+import static javax.ws.rs.core.Response.seeOther;
+import static javax.ws.rs.core.Response.status;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.api.ContentTypes.APPLICATION_ZIP_TYPE;
+import static org.taverna.server.master.api.ContentTypes.DIRECTORY_VARIANTS;
+import static org.taverna.server.master.api.ContentTypes.INITIAL_FILE_VARIANTS;
+import static org.taverna.server.master.common.Roles.SELF;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.PathSegment;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.core.Variant;
+import javax.xml.ws.Holder;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.DirectoryBean;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.DirectoryContents;
+import org.taverna.server.master.rest.FileSegment;
+import org.taverna.server.master.rest.MakeOrUpdateDirEntry;
+import org.taverna.server.master.rest.MakeOrUpdateDirEntry.MakeDirectory;
+import org.taverna.server.master.rest.TavernaServerDirectoryREST;
+import org.taverna.server.master.utils.FilenameUtils;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful access to the filesystem.
+ * 
+ * @author Donal Fellows
+ */
+class DirectoryREST implements TavernaServerDirectoryREST, DirectoryBean {
+       private Log log = getLog("Taverna.Server.Webapp");
+       private TavernaServerSupport support;
+       private TavernaRun run;
+       private FilenameUtils fileUtils;
+
+       @Override
+       public void setSupport(TavernaServerSupport support) {
+               this.support = support;
+       }
+
+       @Override
+       @Required
+       public void setFileUtils(FilenameUtils fileUtils) {
+               this.fileUtils = fileUtils;
+       }
+
+       @Override
+       public DirectoryREST connect(TavernaRun run) {
+               this.run = run;
+               return this;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public Response destroyDirectoryEntry(List<PathSegment> path)
+                       throws NoUpdateException, FilesystemAccessException,
+                       NoDirectoryEntryException {
+               support.permitUpdate(run);
+               fileUtils.getDirEntry(run, path).destroy();
+               return noContent().build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public DirectoryContents getDescription(UriInfo ui)
+                       throws FilesystemAccessException {
+               return new DirectoryContents(ui, run.getWorkingDirectory()
+                               .getContents());
+       }
+
+       @Override
+       @CallCounted
+       public Response options(List<PathSegment> path) {
+               return opt("PUT", "POST", "DELETE");
+       }
+
+       /*
+        * // Nasty! This can have several different responses...
+        * 
+        * @Override @CallCounted private Response
+        * getDirectoryOrFileContents(List<PathSegment> path, UriInfo ui, 
Request
+        * req) throws FilesystemAccessException, NoDirectoryEntryException {
+        * 
+        * DirectoryEntry de = fileUtils.getDirEntry(run, path);
+        * 
+        * // How did the user want the result?
+        * 
+        * List<Variant> variants = getVariants(de); Variant v =
+        * req.selectVariant(variants); if (v == null) return
+        * notAcceptable(variants).type(TEXT_PLAIN)
+        * .entity("Do not know what type of response to produce.") .build();
+        * 
+        * // Produce the content to deliver up
+        * 
+        * Object result; if
+        * (v.getMediaType().equals(APPLICATION_OCTET_STREAM_TYPE))
+        * 
+        * // Only for files...
+        * 
+        * result = de; else if (v.getMediaType().equals(APPLICATION_ZIP_TYPE))
+        * 
+        * // Only for directories...
+        * 
+        * result = ((Directory) de).getContentsAsZip(); else
+        * 
+        * // Only for directories... // XML or JSON; let CXF pick what to do
+        * 
+        * result = new DirectoryContents(ui, ((Directory) de).getContents());
+        * return ok(result).type(v.getMediaType()).build();
+        * 
+        * }
+        */
+
+       private boolean matchType(MediaType a, MediaType b) {
+               if (log.isDebugEnabled())
+                       log.debug("comparing " + a.getType() + "/" + 
a.getSubtype()
+                                       + " and " + b.getType() + "/" + 
b.getSubtype());
+               return (a.isWildcardType() || b.isWildcardType() || 
a.getType().equals(
+                               b.getType()))
+                               && (a.isWildcardSubtype() || 
b.isWildcardSubtype() || a
+                                               
.getSubtype().equals(b.getSubtype()));
+       }
+
+       /**
+        * What are we willing to serve up a directory or file as?
+        * 
+        * @param de
+        *            The reference to the object to serve.
+        * @return The variants we can serve it as.
+        * @throws FilesystemAccessException
+        *             If we fail to read data necessary to detection of its 
media
+        *             type.
+        */
+       private List<Variant> getVariants(DirectoryEntry de)
+                       throws FilesystemAccessException {
+               if (de instanceof Directory)
+                       return DIRECTORY_VARIANTS;
+               else if (!(de instanceof File))
+                       throw new FilesystemAccessException("not a directory or 
file!");
+               File f = (File) de;
+               List<Variant> variants = new ArrayList<>(INITIAL_FILE_VARIANTS);
+               String contentType = support.getEstimatedContentType(f);
+               if (!contentType.equals(APPLICATION_OCTET_STREAM)) {
+                       String[] ct = contentType.split("/");
+                       variants.add(0,
+                                       new Variant(new MediaType(ct[0], 
ct[1]), (String) null, null));
+               }
+               return variants;
+       }
+
+       /** How did the user want the result? */
+       private MediaType pickType(HttpHeaders headers, DirectoryEntry de)
+                       throws FilesystemAccessException, 
NegotiationFailedException {
+               List<Variant> variants = getVariants(de);
+               // Manual content negotiation!!! Ugh!
+               for (MediaType mt : headers.getAcceptableMediaTypes())
+                       for (Variant v : variants)
+                               if (matchType(mt, v.getMediaType()))
+                                       return v.getMediaType();
+               throw new NegotiationFailedException(
+                               "Do not know what type of response to 
produce.", variants);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public Response getDirectoryOrFileContents(List<PathSegment> path,
+                       UriInfo ui, HttpHeaders headers) throws 
FilesystemAccessException,
+                       NoDirectoryEntryException, NegotiationFailedException {
+               DirectoryEntry de = fileUtils.getDirEntry(run, path);
+
+               // How did the user want the result?
+               MediaType wanted = pickType(headers, de);
+
+               log.info("producing content of type " + wanted);
+               // Produce the content to deliver up
+               Object result;
+               if (de instanceof File) {
+                       // Only for files...
+                       result = de;
+                       List<String> range = headers.getRequestHeader("Range");
+                       if (range != null && range.size() == 1)
+                               return new FileSegment((File) de, range.get(0))
+                                               .toResponse(wanted);
+               } else {
+                       // Only for directories...
+                       Directory d = (Directory) de;
+                       if 
(wanted.getType().equals(APPLICATION_ZIP_TYPE.getType())
+                                       && wanted.getSubtype().equals(
+                                                       
APPLICATION_ZIP_TYPE.getSubtype()))
+                               result = d.getContentsAsZip();
+                       else
+                               // XML or JSON; let CXF pick what to do
+                               result = new DirectoryContents(ui, 
d.getContents());
+               }
+               return ok(result).type(wanted).build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public Response makeDirectoryOrUpdateFile(List<PathSegment> parent,
+                       MakeOrUpdateDirEntry op, UriInfo ui) throws 
NoUpdateException,
+                       FilesystemAccessException, NoDirectoryEntryException {
+               support.permitUpdate(run);
+               DirectoryEntry container = fileUtils.getDirEntry(run, parent);
+               if (!(container instanceof Directory))
+                       throw new FilesystemAccessException("You may not "
+                                       + ((op instanceof MakeDirectory) ? 
"make a subdirectory of"
+                                                       : "place a file in") + 
" a file.");
+               if (op.name == null || op.name.length() == 0)
+                       throw new FilesystemAccessException("missing name 
attribute");
+               Directory d = (Directory) container;
+               UriBuilder ub = secure(ui).path("{name}");
+
+               // Make a directory in the context directory
+
+               if (op instanceof MakeDirectory) {
+                       Directory target = 
d.makeSubdirectory(support.getPrincipal(),
+                                       op.name);
+                       return created(ub.build(target.getName())).build();
+               }
+
+               // Make or set the contents of a file
+
+               File f = null;
+               for (DirectoryEntry e : d.getContents()) {
+                       if (e.getName().equals(op.name)) {
+                               if (e instanceof Directory)
+                                       throw new FilesystemAccessException(
+                                                       "You may not overwrite 
a directory with a file.");
+                               f = (File) e;
+                               break;
+                       }
+               }
+               if (f == null) {
+                       f = d.makeEmptyFile(support.getPrincipal(), op.name);
+                       f.setContents(op.contents);
+                       return created(ub.build(f.getName())).build();
+               }
+               f.setContents(op.contents);
+               return seeOther(ub.build(f.getName())).build();
+       }
+
+       private File getFileForWrite(List<PathSegment> filePath,
+                       Holder<Boolean> isNew) throws FilesystemAccessException,
+                       NoDirectoryEntryException, NoUpdateException {
+               support.permitUpdate(run);
+               if (filePath == null || filePath.size() == 0)
+                       throw new FilesystemAccessException(
+                                       "Cannot create a file that is not in a 
directory.");
+
+               List<PathSegment> dirPath = new ArrayList<>(filePath);
+               String name = dirPath.remove(dirPath.size() - 1).getPath();
+               DirectoryEntry de = fileUtils.getDirEntry(run, dirPath);
+               if (!(de instanceof Directory)) {
+                       throw new FilesystemAccessException(
+                                       "Cannot create a file that is not in a 
directory.");
+               }
+               Directory d = (Directory) de;
+
+               File f = null;
+               isNew.value = false;
+               for (DirectoryEntry e : d.getContents())
+                       if (e.getName().equals(name)) {
+                               if (e instanceof File) {
+                                       f = (File) e;
+                                       break;
+                               }
+                               throw new FilesystemAccessException(
+                                               "Cannot create a file that is 
not in a directory.");
+                       }
+
+               if (f == null) {
+                       f = d.makeEmptyFile(support.getPrincipal(), name);
+                       isNew.value = true;
+               } else
+                       f.setContents(new byte[0]);
+               return f;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public Response setFileContents(List<PathSegment> filePath,
+                       InputStream contents, UriInfo ui) throws 
NoDirectoryEntryException,
+                       NoUpdateException, FilesystemAccessException {
+               Holder<Boolean> isNew = new Holder<>(true);
+               support.copyStreamToFile(contents, getFileForWrite(filePath, 
isNew));
+
+               if (isNew.value)
+                       return created(ui.getAbsolutePath()).build();
+               else
+                       return noContent().build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public Response setFileContentsFromURL(List<PathSegment> filePath,
+                       List<URI> referenceList, UriInfo ui)
+                       throws NoDirectoryEntryException, NoUpdateException,
+                       FilesystemAccessException {
+               support.permitUpdate(run);
+               if (referenceList.isEmpty() || referenceList.size() > 1)
+                       return status(422).entity("URI list must have single 
URI in it")
+                                       .build();
+               URI uri = referenceList.get(0);
+               try {
+                       uri.toURL();
+               } catch (MalformedURLException e) {
+                       return status(422).entity("URI list must have value URL 
in it")
+                                       .build();
+               }
+               Holder<Boolean> isNew = new Holder<>(true);
+               File f = getFileForWrite(filePath, isNew);
+
+               try {
+                       support.copyDataToFile(uri, f);
+               } catch (MalformedURLException ex) {
+                       // Should not happen; called uri.toURL() successfully 
above
+                       throw new NoUpdateException("failed to parse URI", ex);
+               } catch (IOException ex) {
+                       throw new FilesystemAccessException(
+                                       "failed to transfer data from URI", ex);
+               }
+
+               if (isNew.value)
+                       return created(ui.getAbsolutePath()).build();
+               else
+                       return noContent().build();
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/FileConcatenation.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/FileConcatenation.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/FileConcatenation.java
new file mode 100644
index 0000000..3893b3d
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/FileConcatenation.java
@@ -0,0 +1,68 @@
+package org.taverna.server.master;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+/**
+ * Simple concatenation of files.
+ * 
+ * @author Donal Fellows
+ */
+public class FileConcatenation implements Iterable<File> {
+       private List<File> files = new ArrayList<>();
+
+       public void add(File f) {
+               files.add(f);
+       }
+
+       public boolean isEmpty() {
+               return files.isEmpty();
+       }
+
+       /**
+        * @return The total length of the files, or -1 if this cannot be
+        *         determined.
+        */
+       public long size() {
+               long size = 0;
+               for (File f : files)
+                       try {
+                               size += f.getSize();
+                       } catch (FilesystemAccessException e) {
+                               // Ignore; shouldn't happen but can't guarantee
+                       }
+               return (size == 0 && !files.isEmpty() ? -1 : size);
+       }
+
+       /**
+        * Get the concatenated files.
+        * 
+        * @param encoding
+        *            The encoding to use.
+        * @return The concatenated files.
+        * @throws UnsupportedEncodingException
+        *             If the encoding doesn't exist.
+        */
+       public String get(String encoding) throws UnsupportedEncodingException {
+               ByteArrayOutputStream baos = new ByteArrayOutputStream();
+               for (File f : files)
+                       try {
+                               baos.write(f.getContents(0, -1));
+                       } catch (FilesystemAccessException | IOException e) {
+                               continue;
+                       }
+               return baos.toString(encoding);
+       }
+
+       @Override
+       public Iterator<File> iterator() {
+               return files.iterator();
+       }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/InputREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/InputREST.java 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/InputREST.java
new file mode 100644
index 0000000..0f48207
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/InputREST.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.util.Date;
+
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.jaxrs.model.URITemplate;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.InputBean;
+import org.taverna.server.master.common.DirEntryReference;
+import org.taverna.server.master.exceptions.BadInputPortNameException;
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+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.NoUpdateException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerInputREST;
+import 
org.taverna.server.master.rest.TavernaServerInputREST.InDesc.AbstractContents;
+import org.taverna.server.master.rest.TavernaServerInputREST.InDesc.Reference;
+import org.taverna.server.master.utils.FilenameUtils;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+import org.taverna.server.port_description.InputDescription;
+
+/**
+ * RESTful interface to the input descriptor of a single workflow run.
+ * 
+ * @author Donal Fellows
+ */
+class InputREST implements TavernaServerInputREST, InputBean {
+       private UriInfo ui;
+       private TavernaServerSupport support;
+       private TavernaRun run;
+       private ContentsDescriptorBuilder cdBuilder;
+       private FilenameUtils fileUtils;
+
+       @Override
+       public void setSupport(TavernaServerSupport support) {
+               this.support = support;
+       }
+
+       @Override
+       @Required
+       public void setCdBuilder(ContentsDescriptorBuilder cdBuilder) {
+               this.cdBuilder = cdBuilder;
+       }
+
+       @Override
+       @Required
+       public void setFileUtils(FilenameUtils fileUtils) {
+               this.fileUtils = fileUtils;
+       }
+
+       @Override
+       public InputREST connect(TavernaRun run, UriInfo ui) {
+               this.run = run;
+               this.ui = ui;
+               return this;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public InputsDescriptor get() {
+               return new InputsDescriptor(ui, run);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public InputDescription getExpected() {
+               return cdBuilder.makeInputDescriptor(run, ui);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public String getBaclavaFile() {
+               String i = run.getInputBaclavaFile();
+               return i == null ? "" : i;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public InDesc getInput(String name, UriInfo ui) throws 
BadInputPortNameException {
+               Input i = support.getInput(run, name);
+               if (i == null)
+                       throw new BadInputPortNameException("unknown input port 
name");
+               return new InDesc(i, ui);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public String setBaclavaFile(String filename) throws NoUpdateException,
+                       BadStateChangeException, FilesystemAccessException {
+               support.permitUpdate(run);
+               run.setInputBaclavaFile(filename);
+               String i = run.getInputBaclavaFile();
+               return i == null ? "" : i;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public InDesc setInput(String name, InDesc inputDescriptor, UriInfo ui)
+                       throws NoUpdateException, BadStateChangeException,
+                       FilesystemAccessException, BadInputPortNameException,
+                       BadPropertyValueException {
+               inputDescriptor.descriptorRef = null;
+               AbstractContents ac = inputDescriptor.assignment;
+               if (name == null || name.isEmpty())
+                       throw new BadInputPortNameException("bad input name");
+               if (ac == null)
+                       throw new BadPropertyValueException("no content!");
+               if (inputDescriptor.delimiter != null
+                               && inputDescriptor.delimiter.isEmpty())
+                       inputDescriptor.delimiter = null;
+               if (ac instanceof InDesc.Reference)
+                       return setRemoteInput(name, (InDesc.Reference) ac,
+                                       inputDescriptor.delimiter, ui);
+               if (!(ac instanceof InDesc.File || ac instanceof InDesc.Value))
+                       throw new BadPropertyValueException("unknown content 
type");
+               support.permitUpdate(run);
+               Input i = support.getInput(run, name);
+               if (i == null)
+                       i = run.makeInput(name);
+               if (ac instanceof InDesc.File)
+                       i.setFile(ac.contents);
+               else
+                       i.setValue(ac.contents);
+               i.setDelimiter(inputDescriptor.delimiter);
+               return new InDesc(i, ui);
+       }
+
+       private InDesc setRemoteInput(String name, Reference ref, String 
delimiter,
+                       UriInfo ui) throws BadStateChangeException,
+                       BadPropertyValueException, FilesystemAccessException {
+               URITemplate tmpl = new URITemplate(ui.getBaseUri()
+                               + "/runs/{runName}/wd/{path:.+}");
+               MultivaluedMap<String, String> mvm = new MetadataMap<>();
+               if (!tmpl.match(ref.contents, mvm)) {
+                       throw new BadPropertyValueException(
+                                       "URI in reference does not refer to 
local disk resource");
+               }
+               try {
+                       File from = fileUtils.getFile(
+                                       
support.getRun(mvm.get("runName").get(0)),
+                                       
SyntheticDirectoryEntry.make(mvm.get("path").get(0)));
+                       File to = run.getWorkingDirectory().makeEmptyFile(
+                                       support.getPrincipal(), 
randomUUID().toString());
+
+                       to.copy(from);
+
+                       Input i = support.getInput(run, name);
+                       if (i == null)
+                               i = run.makeInput(name);
+                       i.setFile(to.getFullName());
+                       i.setDelimiter(delimiter);
+                       return new InDesc(i, ui);
+               } catch (UnknownRunException e) {
+                       throw new BadStateChangeException("may not copy from 
that run", e);
+               } catch (NoDirectoryEntryException e) {
+                       throw new BadStateChangeException("source does not 
exist", e);
+               }
+       }
+
+       @Override
+       @CallCounted
+       public Response options() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response expectedOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response baclavaOptions() {
+               return opt("PUT");
+       }
+
+       @Override
+       @CallCounted
+       public Response inputOptions(@PathParam("name") String name) {
+               return opt("PUT");
+       }
+}
+
+/**
+ * A way to create synthetic directory entries, used during deletion.
+ * 
+ * @author Donal Fellows
+ */
+class SyntheticDirectoryEntry implements DirectoryEntry {
+       public static DirEntryReference make(String path) {
+               return DirEntryReference.newInstance(new 
SyntheticDirectoryEntry(path));
+       }
+
+       private SyntheticDirectoryEntry(String p) {
+               this.p = p;
+               this.d = new Date();
+       }
+
+       private String p;
+       private Date d;
+
+       @Override
+       public String getName() {
+               return null;
+       }
+
+       @Override
+       public String getFullName() {
+               return p;
+       }
+
+       @Override
+       public void destroy() {
+       }
+
+       @Override
+       public int compareTo(DirectoryEntry o) {
+               return p.compareTo(o.getFullName());
+       }
+
+       @Override
+       public Date getModificationDate() {
+               return d;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/InteractionFeed.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/InteractionFeed.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/InteractionFeed.java
new file mode 100644
index 0000000..b686491
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/InteractionFeed.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static org.taverna.server.master.common.Roles.SELF;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.core.Response;
+
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.model.Feed;
+import org.taverna.server.master.api.FeedBean;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interaction.InteractionFeedSupport;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.InteractionFeedREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * How to connect an interaction feed to the webapp.
+ * 
+ * @author Donal Fellows
+ */
+public class InteractionFeed implements InteractionFeedREST, FeedBean {
+       private InteractionFeedSupport interactionFeed;
+       private TavernaRun run;
+
+       @Override
+       public void setInteractionFeedSupport(InteractionFeedSupport feed) {
+               this.interactionFeed = feed;
+       }
+
+       InteractionFeed connect(TavernaRun run) {
+               this.run = run;
+               return this;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public Feed getFeed() throws FilesystemAccessException,
+                       NoDirectoryEntryException {
+               return interactionFeed.getRunFeed(run);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public Response addEntry(Entry entry) throws MalformedURLException,
+                       FilesystemAccessException, NoDirectoryEntryException,
+                       NoUpdateException {
+               Entry realEntry = interactionFeed.addRunFeedEntry(run, entry);
+               URI location;
+               try {
+                       location = realEntry.getSelfLink().getHref().toURI();
+               } catch (URISyntaxException e) {
+                       throw new RuntimeException("failed to make URI from 
link?!", e);
+               }
+               return Response.created(location).entity(realEntry)
+                               
.type("application/atom+xml;type=entry").build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public Entry getEntry(String id) throws FilesystemAccessException,
+                       NoDirectoryEntryException {
+               return interactionFeed.getRunFeedEntry(run, id);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public String deleteEntry(String id) throws FilesystemAccessException,
+                       NoDirectoryEntryException, NoUpdateException {
+               interactionFeed.removeRunFeedEntry(run, id);
+               return "entry successfully deleted";
+       }
+
+       @Override
+       @CallCounted
+       public Response feedOptions() {
+               return opt("POST");
+       }
+
+       @Override
+       @CallCounted
+       public Response entryOptions(String id) {
+               return opt("DELETE");
+       }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/ListenerPropertyREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/ListenerPropertyREST.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/ListenerPropertyREST.java
new file mode 100644
index 0000000..3e983a9
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/ListenerPropertyREST.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.commons.logging.Log;
+import org.taverna.server.master.api.ListenerPropertyBean;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful interface to a single property of a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+class ListenerPropertyREST implements TavernaServerListenersREST.Property,
+               ListenerPropertyBean {
+       private Log log = getLog("Taverna.Server.Webapp");
+       private TavernaServerSupport support;
+       private Listener listen;
+       private String propertyName;
+       private TavernaRun run;
+
+       @Override
+       public void setSupport(TavernaServerSupport support) {
+               this.support = support;
+       }
+
+       @Override
+       public ListenerPropertyREST connect(Listener listen, TavernaRun run,
+                       String propertyName) {
+               this.listen = listen;
+               this.propertyName = propertyName;
+               this.run = run;
+               return this;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public String getValue() {
+               try {
+                       return listen.getProperty(propertyName);
+               } catch (NoListenerException e) {
+                       log.error("unexpected exception; property \"" + 
propertyName
+                                       + "\" should exist", e);
+                       return null;
+               }
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public String setValue(String value) throws NoUpdateException,
+                       NoListenerException {
+               support.permitUpdate(run);
+               listen.setProperty(propertyName, value);
+               return listen.getProperty(propertyName);
+       }
+
+       @Override
+       @CallCounted
+       public Response options() {
+               return opt("PUT");
+       }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/ListenersREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/ListenersREST.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/ListenersREST.java
new file mode 100644
index 0000000..4b7d7f3
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/ListenersREST.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.taverna.server.master.api.ListenersBean;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.ListenerDefinition;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful interface to a single workflow run's event listeners.
+ * 
+ * @author Donal Fellows
+ */
+abstract class ListenersREST implements TavernaServerListenersREST,
+               ListenersBean {
+       private TavernaRun run;
+       private TavernaServerSupport support;
+
+       @Override
+       public void setSupport(TavernaServerSupport support) {
+               this.support = support;
+       }
+
+       @Override
+       public ListenersREST connect(TavernaRun run) {
+               this.run = run;
+               return this;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response addListener(ListenerDefinition typeAndConfiguration,
+                       UriInfo ui) throws NoUpdateException, 
NoListenerException {
+               String name = support.makeListener(run, 
typeAndConfiguration.type,
+                               typeAndConfiguration.configuration).getName();
+               return 
created(secure(ui).path("{listenerName}").build(name)).build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public TavernaServerListenerREST getListener(String name)
+                       throws NoListenerException {
+               Listener l = support.getListener(run, name);
+               if (l == null)
+                       throw new NoListenerException();
+               return makeListenerInterface().connect(l, run);
+       }
+
+       @Nonnull
+       protected abstract SingleListenerREST makeListenerInterface();
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Listeners getDescription(UriInfo ui) {
+               List<ListenerDescription> result = new ArrayList<>();
+               UriBuilder ub = secure(ui).path("{name}");
+               for (Listener l : run.getListeners())
+                       result.add(new ListenerDescription(l,
+                                       fromUri(ub.build(l.getName()))));
+               return new Listeners(result, ub);
+       }
+
+       @Override
+       @CallCounted
+       public Response listenersOptions() {
+               return opt();
+       }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/ManagementState.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/ManagementState.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/ManagementState.java
new file mode 100644
index 0000000..9d4a651
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/ManagementState.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import javax.annotation.PostConstruct;
+import javax.jdo.Query;
+import javax.jdo.annotations.PersistenceAware;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.PrimaryKey;
+
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.ManagementModel;
+import org.taverna.server.master.utils.JDOSupport;
+
+/** The persistent, manageable state of the Taverna Server web application. */
+@PersistenceAware
+class ManagementState extends JDOSupport<WebappState> implements
+               ManagementModel {
+       public ManagementState() {
+               super(WebappState.class);
+       }
+
+       /** Whether we should log all workflows sent to us. */
+       private boolean logIncomingWorkflows = false;
+
+       /** Whether we allow the creation of new workflow runs. */
+       private boolean allowNewWorkflowRuns = true;
+
+       /**
+        * Whether outgoing exceptions should be logged before being converted 
to
+        * responses.
+        */
+       private boolean logOutgoingExceptions = false;
+
+       /**
+        * The file that all usage records should be appended to, or 
<tt>null</tt>
+        * if they should be just dropped.
+        */
+       private String usageRecordLogFile = null;
+
+       @Override
+       public void setLogIncomingWorkflows(boolean logIncomingWorkflows) {
+               this.logIncomingWorkflows = logIncomingWorkflows;
+               if (loadedState)
+                       self.store();
+       }
+
+       @Override
+       public boolean getLogIncomingWorkflows() {
+               self.load();
+               return logIncomingWorkflows;
+       }
+
+       @Override
+       public void setAllowNewWorkflowRuns(boolean allowNewWorkflowRuns) {
+               this.allowNewWorkflowRuns = allowNewWorkflowRuns;
+               if (loadedState)
+                       self.store();
+       }
+
+       @Override
+       public boolean getAllowNewWorkflowRuns() {
+               self.load();
+               return allowNewWorkflowRuns;
+       }
+
+       @Override
+       public void setLogOutgoingExceptions(boolean logOutgoingExceptions) {
+               this.logOutgoingExceptions = logOutgoingExceptions;
+               if (loadedState)
+                       self.store();
+       }
+
+       @Override
+       public boolean getLogOutgoingExceptions() {
+               self.load();
+               return logOutgoingExceptions || true;
+       }
+
+       @Override
+       public String getUsageRecordLogFile() {
+               self.load();
+               return usageRecordLogFile;
+       }
+
+       @Override
+       public void setUsageRecordLogFile(String usageRecordLogFile) {
+               this.usageRecordLogFile = usageRecordLogFile;
+               if (loadedState)
+                       self.store();
+       }
+
+       private static final int KEY = 42; // whatever
+
+       private WebappState get() {
+               Query q = query("id == " + KEY);
+               q.setUnique(true);
+               return (WebappState) q.execute();
+       }
+
+       private boolean loadedState;
+       private ManagementState self;
+
+       @Required
+       public void setSelf(ManagementState self) {
+               this.self = self;
+       }
+
+       @PostConstruct
+       @WithinSingleTransaction
+       public void load() {
+               if (loadedState || !isPersistent())
+                       return;
+               WebappState state = get();
+               if (state == null)
+                       return;
+               allowNewWorkflowRuns = state.getAllowNewWorkflowRuns();
+               logIncomingWorkflows = state.getLogIncomingWorkflows();
+               logOutgoingExceptions = state.getLogOutgoingExceptions();
+               usageRecordLogFile = state.getUsageRecordLogFile();
+               loadedState = true;
+       }
+
+       @WithinSingleTransaction
+       public void store() {
+               if (!isPersistent())
+                       return;
+               WebappState state = get();
+               if (state == null) {
+                       state = new WebappState();
+                       // save state
+                       state.id = KEY; // whatever...
+                       state = persist(state);
+               }
+               state.setAllowNewWorkflowRuns(allowNewWorkflowRuns);
+               state.setLogIncomingWorkflows(logIncomingWorkflows);
+               state.setLogOutgoingExceptions(logOutgoingExceptions);
+               state.setUsageRecordLogFile(usageRecordLogFile);
+               loadedState = true;
+       }
+}
+
+// WARNING! If you change the name of this class, update persistence.xml as
+// well!
+@PersistenceCapable(table = "MANAGEMENTSTATE__WEBAPPSTATE")
+class WebappState implements ManagementModel {
+       public WebappState() {
+       }
+
+       @PrimaryKey
+       protected int id;
+
+       /** Whether we should log all workflows sent to us. */
+       @Persistent
+       private boolean logIncomingWorkflows;
+
+       /** Whether we allow the creation of new workflow runs. */
+       @Persistent
+       private boolean allowNewWorkflowRuns;
+
+       /**
+        * Whether outgoing exceptions should be logged before being converted 
to
+        * responses.
+        */
+       @Persistent
+       private boolean logOutgoingExceptions;
+
+       /** Where to write usage records. */
+       @Persistent
+       private String usageRecordLogFile;
+
+       @Override
+       public void setLogIncomingWorkflows(boolean logIncomingWorkflows) {
+               this.logIncomingWorkflows = logIncomingWorkflows;
+       }
+
+       @Override
+       public boolean getLogIncomingWorkflows() {
+               return logIncomingWorkflows;
+       }
+
+       @Override
+       public void setAllowNewWorkflowRuns(boolean allowNewWorkflowRuns) {
+               this.allowNewWorkflowRuns = allowNewWorkflowRuns;
+       }
+
+       @Override
+       public boolean getAllowNewWorkflowRuns() {
+               return allowNewWorkflowRuns;
+       }
+
+       @Override
+       public void setLogOutgoingExceptions(boolean logOutgoingExceptions) {
+               this.logOutgoingExceptions = logOutgoingExceptions;
+       }
+
+       @Override
+       public boolean getLogOutgoingExceptions() {
+               return logOutgoingExceptions;
+       }
+
+       @Override
+       public String getUsageRecordLogFile() {
+               return usageRecordLogFile;
+       }
+
+       @Override
+       public void setUsageRecordLogFile(String usageRecordLogFile) {
+               this.usageRecordLogFile = usageRecordLogFile;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/RunREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/RunREST.java 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/RunREST.java
new file mode 100644
index 0000000..563a822
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/RunREST.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.ok;
+import static javax.ws.rs.core.Response.status;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.joda.time.format.ISODateTimeFormat.dateTime;
+import static org.joda.time.format.ISODateTimeFormat.dateTimeParser;
+import static org.taverna.server.master.common.Roles.SELF;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.common.Status.Initialized;
+import static org.taverna.server.master.common.Status.Operating;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.util.Date;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.logging.Log;
+import org.joda.time.DateTime;
+import org.ogf.usage.JobUsageRecord;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.RunBean;
+import org.taverna.server.master.common.ProfileList;
+import org.taverna.server.master.common.Status;
+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.exceptions.OverloadedException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.rest.InteractionFeedREST;
+import org.taverna.server.master.rest.TavernaServerInputREST;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+import org.taverna.server.master.rest.TavernaServerRunREST;
+import org.taverna.server.master.rest.TavernaServerSecurityREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+import org.taverna.server.port_description.OutputDescription;
+
+/**
+ * RESTful interface to a single workflow run.
+ * 
+ * @author Donal Fellows
+ */
+abstract class RunREST implements TavernaServerRunREST, RunBean {
+       private Log log = getLog("Taverna.Server.Webapp");
+       private String runName;
+       private TavernaRun run;
+       private TavernaServerSupport support;
+       private ContentsDescriptorBuilder cdBuilder;
+
+       @Override
+       @Required
+       public void setSupport(TavernaServerSupport support) {
+               this.support = support;
+       }
+
+       @Override
+       @Required
+       public void setCdBuilder(ContentsDescriptorBuilder cdBuilder) {
+               this.cdBuilder = cdBuilder;
+       }
+
+       @Override
+       public void setRunName(String runName) {
+               this.runName = runName;
+       }
+
+       @Override
+       public void setRun(TavernaRun run) {
+               this.run = run;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public RunDescription getDescription(UriInfo ui) {
+               return new RunDescription(run, ui);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public Response destroy() throws NoUpdateException {
+               try {
+                       support.unregisterRun(runName, run);
+               } catch (UnknownRunException e) {
+                       log.fatal("can't happen", e);
+               }
+               return noContent().build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public TavernaServerListenersREST getListeners() {
+               return makeListenersInterface().connect(run);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public TavernaServerSecurityREST getSecurity() throws NotOwnerException 
{
+               TavernaSecurityContext secContext = run.getSecurityContext();
+               if (!support.getPrincipal().equals(secContext.getOwner()))
+                       throw new NotOwnerException();
+
+               // context.getBean("run.security", run, secContext);
+               return makeSecurityInterface().connect(secContext, run);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getExpiryTime() {
+               return dateTime().print(new DateTime(run.getExpiry()));
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getCreateTime() {
+               return dateTime().print(new 
DateTime(run.getCreationTimestamp()));
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getFinishTime() {
+               Date f = run.getFinishTimestamp();
+               return f == null ? "" : dateTime().print(new DateTime(f));
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getStartTime() {
+               Date f = run.getStartTimestamp();
+               return f == null ? "" : dateTime().print(new DateTime(f));
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getStatus() {
+               return run.getStatus().toString();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public Workflow getWorkflow() {
+               return run.getWorkflow();
+       }
+
+       @Override
+       @CallCounted
+       public String getMainProfileName() {
+               String name = run.getWorkflow().getMainProfileName();
+               return (name == null ? "" : name);
+       }
+
+       @Override
+       @CallCounted
+       public ProfileList getProfiles() {
+               return support.getProfileDescriptor(run.getWorkflow());
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public DirectoryREST getWorkingDirectory() {
+               return makeDirectoryInterface().connect(run);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String setExpiryTime(String expiry) throws NoUpdateException,
+                       IllegalArgumentException {
+               DateTime wanted = dateTimeParser().parseDateTime(expiry.trim());
+               Date achieved = support.updateExpiry(run, wanted.toDate());
+               return dateTime().print(new DateTime(achieved));
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public Response setStatus(String status) throws NoUpdateException {
+               Status newStatus = Status.valueOf(status.trim());
+               support.permitUpdate(run);
+               if (newStatus == Operating && run.getStatus() == Initialized) {
+                       if (!support.getAllowStartWorkflowRuns())
+                               throw new OverloadedException();
+                       String issue = run.setStatus(newStatus);
+                       if (issue == null)
+                               issue = "starting run...";
+                       return 
status(202).entity(issue).type("text/plain").build();
+               }
+               run.setStatus(newStatus); // Ignore the result
+               return 
ok(run.getStatus().toString()).type("text/plain").build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public TavernaServerInputREST getInputs(UriInfo ui) {
+               return makeInputInterface().connect(run, ui);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getOutputFile() {
+               String o = run.getOutputBaclavaFile();
+               return o == null ? "" : o;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String setOutputFile(String filename) throws NoUpdateException,
+                       FilesystemAccessException, BadStateChangeException {
+               support.permitUpdate(run);
+               if (filename != null && filename.length() == 0)
+                       filename = null;
+               run.setOutputBaclavaFile(filename);
+               String o = run.getOutputBaclavaFile();
+               return o == null ? "" : o;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public OutputDescription getOutputDescription(UriInfo ui)
+                       throws BadStateChangeException, 
FilesystemAccessException,
+                       NoDirectoryEntryException {
+               if (run.getStatus() == Initialized)
+                       throw new BadStateChangeException(
+                                       "may not get output description in 
initial state");
+               return cdBuilder.makeOutputDescriptor(run, ui);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed({ USER, SELF })
+       public InteractionFeedREST getInteractionFeed() {
+               return makeInteractionFeed().connect(run);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getName() {
+               return run.getName();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String setName(String name) throws NoUpdateException {
+               support.permitUpdate(run);
+               run.setName(name);
+               return run.getName();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getStdout() throws NoListenerException {
+               return support.getProperty(run, "io", "stdout");
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public String getStderr() throws NoListenerException {
+               return support.getProperty(run, "io", "stderr");
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public Response getUsage() throws NoListenerException, JAXBException {
+               String ur = support.getProperty(run, "io", "usageRecord");
+               if (ur.isEmpty())
+                       return noContent().build();
+               return ok(JobUsageRecord.unmarshal(ur), 
APPLICATION_XML).build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public Response getLogContents() {
+               FileConcatenation fc = support.getLogs(run);
+               if (fc.isEmpty())
+                       return Response.noContent().build();
+               return Response.ok(fc, TEXT_PLAIN).build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public Response getRunBundle() {
+               FileConcatenation fc = support.getProv(run);
+               if (fc.isEmpty())
+                       return Response.status(404).entity("no provenance 
currently available").build();
+               return Response.ok(fc, 
"application/vnd.wf4ever.robundle+zip").build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public boolean getGenerateProvenance() {
+               return run.getGenerateProvenance();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       @RolesAllowed(USER)
+       public boolean setGenerateProvenance(boolean newValue) throws 
NoUpdateException {
+               support.permitUpdate(run);
+               run.setGenerateProvenance(newValue);
+               return run.getGenerateProvenance();
+       }
+
+       /**
+        * Construct a RESTful interface to a run's filestore.
+        * 
+        * @return The handle to the interface, as decorated by Spring.
+        */
+       protected abstract DirectoryREST makeDirectoryInterface();
+
+       /**
+        * Construct a RESTful interface to a run's input descriptors.
+        * 
+        * @return The handle to the interface, as decorated by Spring.
+        */
+       protected abstract InputREST makeInputInterface();
+
+       /**
+        * Construct a RESTful interface to a run's listeners.
+        * 
+        * @return The handle to the interface, as decorated by Spring.
+        */
+       protected abstract ListenersREST makeListenersInterface();
+
+       /**
+        * Construct a RESTful interface to a run's security.
+        * 
+        * @return The handle to the interface, as decorated by Spring.
+        */
+       protected abstract RunSecurityREST makeSecurityInterface();
+
+       /**
+        * Construct a RESTful interface to a run's interaction feed.
+        * 
+        * @return The handle to the interaface, as decorated by Spring.
+        */
+       protected abstract InteractionFeed makeInteractionFeed();
+
+       @Override
+       @CallCounted
+       public Response runOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response workflowOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response profileOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response expiryOptions() {
+               return opt("PUT");
+       }
+
+       @Override
+       @CallCounted
+       public Response createTimeOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response startTimeOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response finishTimeOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response statusOptions() {
+               return opt("PUT");
+       }
+
+       @Override
+       @CallCounted
+       public Response outputOptions() {
+               return opt("PUT");
+       }
+
+       @Override
+       @CallCounted
+       public Response nameOptions() {
+               return opt("PUT");
+       }
+
+       @Override
+       @CallCounted
+       public Response stdoutOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response stderrOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response usageOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response logOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response runBundleOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response generateProvenanceOptions() {
+               return opt("PUT");
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/RunSecurityREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/RunSecurityREST.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/RunSecurityREST.java
new file mode 100644
index 0000000..5a366b2
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/RunSecurityREST.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static java.util.UUID.randomUUID;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static org.taverna.server.master.common.Status.Initialized;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.net.URI;
+import java.util.Map;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.taverna.server.master.api.SecurityBean;
+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.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.exceptions.NoCredentialException;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.rest.TavernaServerSecurityREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful interface to a single workflow run's security settings.
+ * 
+ * @author Donal Fellows
+ */
+class RunSecurityREST implements TavernaServerSecurityREST, SecurityBean {
+       private TavernaServerSupport support;
+       private TavernaSecurityContext context;
+       private TavernaRun run;
+
+       @Override
+       public void setSupport(TavernaServerSupport support) {
+               this.support = support;
+       }
+
+       @Override
+       public RunSecurityREST connect(TavernaSecurityContext context,
+                       TavernaRun run) {
+               this.context = context;
+               this.run = run;
+               return this;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Descriptor describe(UriInfo ui) {
+               return new Descriptor(secure(ui).path("{element}"), 
context.getOwner()
+                               .getName(), context.getCredentials(), 
context.getTrusted());
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public String getOwner() {
+               return context.getOwner().getName();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public CredentialList listCredentials() {
+               return new CredentialList(context.getCredentials());
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public CredentialHolder getParticularCredential(String id)
+                       throws NoCredentialException {
+               for (Credential c : context.getCredentials())
+                       if (c.id.equals(id))
+                               return new CredentialHolder(c);
+               throw new NoCredentialException();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public CredentialHolder setParticularCredential(String id,
+                       CredentialHolder cred, UriInfo ui)
+                       throws InvalidCredentialException, 
BadStateChangeException {
+               if (run.getStatus() != Initialized)
+                       throw new BadStateChangeException();
+               Credential c = cred.credential;
+               c.id = id;
+               c.href = ui.getAbsolutePath().toString();
+               context.validateCredential(c);
+               context.deleteCredential(c);
+               context.addCredential(c);
+               return new CredentialHolder(c);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response addCredential(CredentialHolder cred, UriInfo ui)
+                       throws InvalidCredentialException, 
BadStateChangeException {
+               if (run.getStatus() != Initialized)
+                       throw new BadStateChangeException();
+               Credential c = cred.credential;
+               c.id = randomUUID().toString();
+               URI uri = secure(ui).path("{id}").build(c.id);
+               c.href = uri.toString();
+               context.validateCredential(c);
+               context.addCredential(c);
+               return created(uri).build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response deleteAllCredentials(UriInfo ui)
+                       throws BadStateChangeException {
+               if (run.getStatus() != Initialized)
+                       throw new BadStateChangeException();
+               for (Credential c : context.getCredentials())
+                       context.deleteCredential(c);
+               return noContent().build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response deleteCredential(String id, UriInfo ui)
+                       throws BadStateChangeException {
+               if (run.getStatus() != Initialized)
+                       throw new BadStateChangeException();
+               context.deleteCredential(new Credential.Dummy(id));
+               return noContent().build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public TrustList listTrusted() {
+               return new TrustList(context.getTrusted());
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Trust getParticularTrust(String id) throws NoCredentialException 
{
+               for (Trust t : context.getTrusted())
+                       if (t.id.equals(id))
+                               return t;
+               throw new NoCredentialException();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Trust setParticularTrust(String id, Trust t, UriInfo ui)
+                       throws InvalidCredentialException, 
BadStateChangeException {
+               if (run.getStatus() != Initialized)
+                       throw new BadStateChangeException();
+               t.id = id;
+               t.href = ui.getAbsolutePath().toString();
+               context.validateTrusted(t);
+               context.deleteTrusted(t);
+               context.addTrusted(t);
+               return t;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response addTrust(Trust t, UriInfo ui)
+                       throws InvalidCredentialException, 
BadStateChangeException {
+               if (run.getStatus() != Initialized)
+                       throw new BadStateChangeException();
+               t.id = randomUUID().toString();
+               URI uri = secure(ui).path("{id}").build(t.id);
+               t.href = uri.toString();
+               context.validateTrusted(t);
+               context.addTrusted(t);
+               return created(uri).build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response deleteAllTrusts(UriInfo ui) throws 
BadStateChangeException {
+               if (run.getStatus() != Initialized)
+                       throw new BadStateChangeException();
+               for (Trust t : context.getTrusted())
+                       context.deleteTrusted(t);
+               return noContent().build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response deleteTrust(String id, UriInfo ui)
+                       throws BadStateChangeException {
+               if (run.getStatus() != Initialized)
+                       throw new BadStateChangeException();
+               Trust toDelete = new Trust();
+               toDelete.id = id;
+               context.deleteTrusted(toDelete);
+               return noContent().build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public PermissionsDescription describePermissions(UriInfo ui) {
+               Map<String, Permission> perm = 
support.getPermissionMap(context);
+               return new PermissionsDescription(secure(ui).path("{id}"), 
perm);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Permission describePermission(String id) {
+               return support.getPermission(context, id);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Permission setPermission(String id, Permission perm) {
+               support.setPermission(context, id, perm);
+               return support.getPermission(context, id);
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response deletePermission(String id, UriInfo ui) {
+               support.setPermission(context, id, Permission.None);
+               return noContent().build();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public Response makePermission(PermissionDescription desc, UriInfo ui) {
+               support.setPermission(context, desc.userName, desc.permission);
+               return 
created(secure(ui).path("{user}").build(desc.userName)).build();
+       }
+
+       @Override
+       @CallCounted
+       public Response descriptionOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response ownerOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response credentialsOptions() {
+               return opt("POST", "DELETE");
+       }
+
+       @Override
+       @CallCounted
+       public Response credentialOptions(String id) {
+               return opt("PUT", "DELETE");
+       }
+
+       @Override
+       @CallCounted
+       public Response trustsOptions() {
+               return opt("POST", "DELETE");
+       }
+
+       @Override
+       @CallCounted
+       public Response trustOptions(String id) {
+               return opt("PUT", "DELETE");
+       }
+
+       @Override
+       @CallCounted
+       public Response permissionsOptions() {
+               return opt("POST");
+       }
+
+       @Override
+       @CallCounted
+       public Response permissionOptions(String id) {
+               return opt("PUT", "DELETE");
+       }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/SingleListenerREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/SingleListenerREST.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/SingleListenerREST.java
new file mode 100644
index 0000000..6c9e8d8
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/SingleListenerREST.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static java.util.Arrays.asList;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.taverna.server.master.api.OneListenerBean;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+import 
org.taverna.server.master.rest.TavernaServerListenersREST.ListenerDescription;
+import 
org.taverna.server.master.rest.TavernaServerListenersREST.TavernaServerListenerREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful interface to a single listener attached to a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+abstract class SingleListenerREST implements TavernaServerListenerREST,
+               OneListenerBean {
+       private Listener listen;
+       private TavernaRun run;
+
+       @Override
+       public SingleListenerREST connect(Listener listen, TavernaRun run) {
+               this.listen = listen;
+               this.run = run;
+               return this;
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public String getConfiguration() {
+               return listen.getConfiguration();
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public ListenerDescription getDescription(UriInfo ui) {
+               return new ListenerDescription(listen, secure(ui));
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public TavernaServerListenersREST.Properties getProperties(UriInfo ui) {
+               return new 
TavernaServerListenersREST.Properties(secure(ui).path(
+                               "{prop}"), listen.listProperties());
+       }
+
+       @Override
+       @CallCounted
+       @PerfLogged
+       public TavernaServerListenersREST.Property getProperty(
+                       final String propertyName) throws NoListenerException {
+               List<String> p = asList(listen.listProperties());
+               if (p.contains(propertyName)) {
+                       return makePropertyInterface().connect(listen, run, 
propertyName);
+               }
+               throw new NoListenerException("no such property");
+       }
+
+       protected abstract ListenerPropertyREST makePropertyInterface();
+
+       @Override
+       @CallCounted
+       public Response listenerOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response configurationOptions() {
+               return opt();
+       }
+
+       @Override
+       @CallCounted
+       public Response propertiesOptions() {
+               return opt();
+       }
+}

Reply via email to