http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/JabberDispatcher.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/JabberDispatcher.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/JabberDispatcher.java
new file mode 100644
index 0000000..61640bf
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/JabberDispatcher.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+
+package org.taverna.server.master.notification;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jivesoftware.smack.Chat;
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.MessageListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.packet.Message;
+import org.taverna.server.master.interfaces.MessageDispatcher;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * Send notifications by Jabber/XMPP.
+ * 
+ * @author Donal Fellows
+ */
+public class JabberDispatcher implements MessageDispatcher {
+       @Override
+       public String getName() {
+               return "xmpp";
+       }
+
+       private Log log = LogFactory.getLog("Taverna.Server.Notification");
+       private XMPPConnection conn;
+       private String resource = "TavernaServer";
+       private String host = "";
+       private String user = "";
+       private String pass = "";
+
+       /**
+        * @param resource
+        *            The XMPP resource to use when connecting the server. This
+        *            defaults to "<tt>TavernaServer</tt>".
+        */
+       public void setResource(String resource) {
+               this.resource = resource;
+       }
+
+       /**
+        * @param service
+        *            The XMPP service URL.
+        */
+       public void setHost(String service) {
+               if (service == null || service.trim().isEmpty()
+                               || service.trim().startsWith("$"))
+                       this.host = "";
+               else
+                       this.host = service.trim();
+       }
+
+       /**
+        * @param user
+        *            The user identity to use with the XMPP service.
+        */
+       public void setUsername(String user) {
+               if (user == null || user.trim().isEmpty()
+                               || user.trim().startsWith("$"))
+                       this.user = "";
+               else
+                       this.user = user.trim();
+       }
+
+       /**
+        * @param pass
+        *            The password to use with the XMPP service.
+        */
+       public void setPassword(String pass) {
+               if (pass == null || pass.trim().isEmpty()
+                               || pass.trim().startsWith("$"))
+                       this.pass = "";
+               else
+                       this.pass = pass.trim();
+       }
+
+       @PostConstruct
+       void setup() {
+               try {
+                       if (host.isEmpty() || user.isEmpty() || pass.isEmpty()) 
{
+                               log.info("disabling XMPP support; incomplete 
configuration");
+                               conn = null;
+                               return;
+                       }
+                       ConnectionConfiguration cfg = new 
ConnectionConfiguration(host);
+                       cfg.setSendPresence(false);
+                       XMPPConnection c = new XMPPConnection(cfg);
+                       c.connect();
+                       c.login(user, pass, resource);
+                       conn = c;
+                       log.info("connected to XMPP service <" + host + "> as 
user <"
+                                       + user + ">");
+               } catch (Exception e) {
+                       log.info("failed to connect to XMPP server", e);
+               }
+       }
+
+       @PreDestroy
+       public void close() {
+               if (conn != null)
+                       conn.disconnect();
+               conn = null;
+       }
+
+       @Override
+       public boolean isAvailable() {
+               return conn != null;
+       }
+
+       @Override
+       public void dispatch(TavernaRun ignored, String messageSubject,
+                       String messageContent, String targetParameter) throws 
Exception {
+               Chat chat = conn.getChatManager().createChat(targetParameter,
+                               new DroppingListener());
+               Message m = new Message();
+               m.addBody(null, messageContent);
+               m.setSubject(messageSubject);
+               chat.sendMessage(m);
+       }
+
+       static class DroppingListener implements MessageListener {
+               private Log log = LogFactory
+                               .getLog("Taverna.Server.Notification.Jabber");
+
+               @Override
+               public void processMessage(Chat chat, Message message) {
+                       if (log.isDebugEnabled())
+                               log.debug("unexpectedly received XMPP message 
from <"
+                                               + message.getFrom() + ">; 
ignoring");
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/NotificationEngine.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/NotificationEngine.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/NotificationEngine.java
new file mode 100644
index 0000000..bc0f60d
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/NotificationEngine.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.interfaces.MessageDispatcher;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * A common object for handling dispatch of event-driven messages.
+ * 
+ * @author Donal Fellows
+ */
+public class NotificationEngine {
+       private Log log = LogFactory.getLog("Taverna.Server.Notification");
+       private Map<String, MessageDispatcher> dispatchers;
+       private List<MessageDispatcher> universalDispatchers;
+
+       /**
+        * @param dispatchers
+        *            The various dispatchers we want to install.
+        */
+       @Required
+       public void setDispatchers(List<MessageDispatcher> dispatchers) {
+               this.dispatchers = new HashMap<>();
+               for (MessageDispatcher d : dispatchers)
+                       this.dispatchers.put(d.getName(), d);
+       }
+
+       /**
+        * @param dispatcherList
+        *            A list of dispatch objects to always dispatch to.
+        */
+       @Required
+       public void setUniversalDispatchers(List<MessageDispatcher> 
dispatcherList) {
+               this.universalDispatchers = dispatcherList;
+       }
+
+       private void dispatchToChosenTarget(TavernaRun originator, String 
scheme,
+                       String target, Message message) throws Exception {
+               try {
+                       MessageDispatcher d = dispatchers.get(scheme);
+                       if (d != null && d.isAvailable())
+                               d.dispatch(originator, message.getTitle(scheme),
+                                               message.getContent(scheme), 
target);
+                       else
+                               log.warn("no such notification dispatcher for " 
+ scheme);
+               } catch (URISyntaxException e) {
+                       // See if *someone* will handle the message
+                       Exception e2 = null;
+                       for (MessageDispatcher d : dispatchers.values())
+                               try {
+                                       if (d.isAvailable()) {
+                                               d.dispatch(originator, 
message.getTitle(d.getName()),
+                                                               
message.getContent(d.getName()), scheme + ":"
+                                                                               
+ target);
+                                               return;
+                                       }
+                               } catch (Exception ex) {
+                                       if (log.isDebugEnabled())
+                                               log.debug("failed in 
pseudo-directed dispatch of "
+                                                               + scheme + ":" 
+ target, ex);
+                                       e2 = ex;
+                               }
+                       if (e2 != null)
+                               throw e2;
+               }
+       }
+
+       private void dispatchUniversally(TavernaRun originator, Message message)
+                       throws Exception {
+               for (MessageDispatcher d : universalDispatchers)
+                       try {
+                               if (d.isAvailable())
+                                       d.dispatch(originator, 
message.getTitle(d.getName()),
+                                                       
message.getContent(d.getName()), null);
+                       } catch (Exception e) {
+                               log.warn("problem in universal dispatcher", e);
+                       }
+       }
+
+       /**
+        * Dispatch a message over the notification fabric.
+        * 
+        * @param originator
+        *            What workflow run was the source of this message?
+        * @param destination
+        *            Where the message should get delivered to. The correct 
format
+        *            of this is either as a URI of some form (where the scheme
+        *            determines the dispatcher) or as an invalid URI in which 
case
+        *            it is just tried against the possibilities to see if any
+        *            succeeds.
+        * @param subject
+        *            The subject line of the message.
+        * @param message
+        *            The plain text body of the message.
+        * @throws Exception
+        *             If anything goes wrong with the dispatch process.
+        */
+       public void dispatchMessage(TavernaRun originator, String destination,
+                       Message message) throws Exception {
+               if (destination != null && !destination.trim().isEmpty()) {
+                       try {
+                               URI toURI = new URI(destination.trim());
+                               dispatchToChosenTarget(originator, 
toURI.getScheme(),
+                                               toURI.getSchemeSpecificPart(), 
message);
+                       } catch (URISyntaxException e) {
+                               // Ignore
+                       }
+               }
+               dispatchUniversally(originator, message);
+       }
+
+       /**
+        * @return The message dispatchers that are actually available (i.e., 
not
+        *         disabled by configuration somewhere).
+        */
+       public List<String> listAvailableDispatchers() {
+               ArrayList<String> result = new ArrayList<>();
+               for (Map.Entry<String, MessageDispatcher> entry : dispatchers
+                               .entrySet()) {
+                       if (entry.getValue().isAvailable())
+                               result.add(entry.getKey());
+               }
+               return result;
+       }
+
+       public interface Message {
+               String getContent(String type);
+
+               String getTitle(String type);
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/RateLimitedDispatcher.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/RateLimitedDispatcher.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/RateLimitedDispatcher.java
new file mode 100644
index 0000000..c8d7ef6
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/RateLimitedDispatcher.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.joda.time.DateTime;
+import org.taverna.server.master.interfaces.MessageDispatcher;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * Rate-limiting support. Some message fabrics simply should not be used to 
send
+ * a lot of messages.
+ * 
+ * @author Donal Fellows
+ */
+public abstract class RateLimitedDispatcher implements MessageDispatcher {
+       /** Pre-configured logger. */
+       protected Log log = LogFactory.getLog("Taverna.Server.Notification");
+       private int cooldownSeconds;
+       private Map<String, DateTime> lastSend = new HashMap<>();
+
+       String valid(String value, String def) {
+               if (value == null || value.trim().isEmpty()
+                               || value.trim().startsWith("${"))
+                       return def;
+               else
+                       return value.trim();
+       }
+
+       /**
+        * Set how long must elapse between updates to the status of any 
particular
+        * user. Calls before that time are just silently dropped.
+        * 
+        * @param cooldownSeconds
+        *            Time to elapse, in seconds.
+        */
+       public void setCooldownSeconds(int cooldownSeconds) {
+               this.cooldownSeconds = cooldownSeconds;
+       }
+
+       /**
+        * Test whether the rate limiter allows the given user to send a 
message.
+        * 
+        * @param who
+        *            Who wants to send the message?
+        * @return <tt>true</tt> iff they are permitted.
+        */
+       protected boolean isSendAllowed(String who) {
+               DateTime now = new DateTime();
+               synchronized (lastSend) {
+                       DateTime last = lastSend.get(who);
+                       if (last != null) {
+                               if 
(!now.isAfter(last.plusSeconds(cooldownSeconds)))
+                                       return false;
+                       }
+                       lastSend.put(who, now);
+               }
+               return true;
+       }
+
+       @Override
+       public void dispatch(TavernaRun ignored, String messageSubject,
+                       String messageContent, String target) throws Exception {
+               if (isSendAllowed(target))
+                       dispatch(messageSubject, messageContent, target);
+       }
+
+       /**
+        * Dispatch a message to a recipient that doesn't care what produced it.
+        * 
+        * @param messageSubject
+        *            The subject of the message to send.
+        * @param messageContent
+        *            The plain-text content of the message to send.
+        * @param target
+        *            A description of where it is to go.
+        * @throws Exception
+        *             If anything goes wrong.
+        */
+       public abstract void dispatch(String messageSubject, String 
messageContent,
+                       String target) throws Exception;
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/SMSDispatcher.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/SMSDispatcher.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/SMSDispatcher.java
new file mode 100644
index 0000000..5553141
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/SMSDispatcher.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import static org.taverna.server.master.defaults.Default.SMS_GATEWAY_URL;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.springframework.beans.factory.annotation.Required;
+
+/**
+ * Dispatch termination messages via SMS.
+ * 
+ * @author Donal Fellows
+ */
+public class SMSDispatcher extends RateLimitedDispatcher {
+       @Override
+       public String getName() {
+               return "sms";
+       }
+
+       private CloseableHttpClient client;
+       private URI service;
+       private String user = "", pass = "";
+       private String usernameField = "username", passwordField = "password",
+                       destinationField = "to", messageField = "text";
+
+       /**
+        * @param usernameField
+        *            The name of the field that conveys the sending username; 
this
+        *            is the <i>server</i>'s identity.
+        */
+       @Required
+       public void setUsernameField(String usernameField) {
+               this.usernameField = usernameField;
+       }
+
+       /**
+        * @param passwordField
+        *            The field holding the password to authenticate the server 
to
+        *            the SMS gateway.
+        */
+       @Required
+       public void setPasswordField(String passwordField) {
+               this.passwordField = passwordField;
+       }
+
+       /**
+        * @param destinationField
+        *            The field holding the number to send the SMS to.
+        */
+       @Required
+       public void setDestinationField(String destinationField) {
+               this.destinationField = destinationField;
+       }
+
+       /**
+        * @param messageField
+        *            The field holding the plain-text message to send.
+        */
+       @Required
+       public void setMessageField(String messageField) {
+               this.messageField = messageField;
+       }
+
+       public void setService(String serviceURL) {
+               String s = valid(serviceURL, "");
+               if (s.isEmpty()) {
+                       log.warn("did not get sms.service from servlet config; 
using default ("
+                                       + SMS_GATEWAY_URL + ")");
+                       s = SMS_GATEWAY_URL;
+               }
+               try {
+                       service = new URI(s);
+               } catch (URISyntaxException e) {
+                       service = null;
+               }
+       }
+
+       public void setUser(String user) {
+               this.user = valid(user, "");
+       }
+
+       public void setPassword(String pass) {
+               this.pass = valid(pass, "");
+       }
+
+       @PostConstruct
+       void init() {
+               client = HttpClientBuilder.create().build();
+       }
+
+       @PreDestroy
+       void close() throws IOException {
+               try {
+                       if (client != null)
+                               client.close();
+               } finally {
+                       client = null;
+               }
+       }
+
+       @Override
+       public boolean isAvailable() {
+               return service != null && !user.isEmpty() && !pass.isEmpty();
+       }
+
+       @Override
+       public void dispatch(String messageSubject, String messageContent,
+                       String targetParameter) throws Exception {
+               // Sanity check
+               if (!targetParameter.matches("[^0-9]+"))
+                       throw new Exception("invalid phone number");
+
+               if (!isSendAllowed("anyone"))
+                       return;
+
+               // Build the message to send
+               List<NameValuePair> params = new ArrayList<>();
+               params.add(new BasicNameValuePair(usernameField, user));
+               params.add(new BasicNameValuePair(passwordField, pass));
+               params.add(new BasicNameValuePair(destinationField, 
targetParameter));
+               params.add(new BasicNameValuePair(messageField, 
messageContent));
+
+               // Send the message
+               HttpPost post = new HttpPost(service);
+               post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
+               HttpResponse response = client.execute(post);
+
+               // Log the response
+               HttpEntity entity = response.getEntity();
+               if (entity != null)
+                       try (BufferedReader e = new BufferedReader(new 
InputStreamReader(
+                                       entity.getContent()))) {
+                               log.info(e.readLine());
+                       }
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/TwitterDispatcher.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/TwitterDispatcher.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/TwitterDispatcher.java
new file mode 100644
index 0000000..8ee4815
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/TwitterDispatcher.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import java.util.Properties;
+
+import twitter4j.Twitter;
+import twitter4j.TwitterFactory;
+import twitter4j.conf.Configuration;
+import twitter4j.conf.PropertyConfiguration;
+import twitter4j.auth.AuthorizationFactory;
+
+/**
+ * Super simple-minded twitter dispatcher. You need to tell it your consumer 
key
+ * and secret as part of the connection parameters, for example via a 
dispatcher
+ * URN of "<tt>twitter:fred:bloggs</tt>" where <tt>fred</tt> is the key and
+ * <tt>bloggs</tt> is the secret.
+ * 
+ * @author Donal Fellows
+ */
+public class TwitterDispatcher extends RateLimitedDispatcher {
+       @Override
+       public String getName() {
+               return "twitter";
+       }
+
+       public static final int MAX_MESSAGE_LENGTH = 140;
+       public static final char ELLIPSIS = '\u2026';
+
+       private String token = "";
+       private String secret = "";
+
+       public void setAccessToken(String token) {
+               this.token = valid(token, "");
+       }
+
+       public void setAccessSecret(String secret) {
+               this.secret = valid(secret, "");
+       }
+
+       private Properties getConfig() throws NotConfiguredException {
+               if (token.isEmpty() || secret.isEmpty())
+                       throw new NotConfiguredException();
+               Properties p = new Properties();
+               p.setProperty(ACCESS_TOKEN_PROP, token);
+               p.setProperty(ACCESS_SECRET_PROP, secret);
+               return p;
+       }
+
+       public static final String ACCESS_TOKEN_PROP = "oauth.accessToken";
+       public static final String ACCESS_SECRET_PROP = 
"oauth.accessTokenSecret";
+
+       private Twitter getTwitter(String key, String secret) throws Exception {
+               if (key.isEmpty() || secret.isEmpty())
+                       throw new NoCredentialsException();
+
+               Properties p = getConfig();
+               p.setProperty("oauth.consumerKey", key);
+               p.setProperty("oauth.consumerSecret", secret);
+
+               Configuration config = new PropertyConfiguration(p);
+               TwitterFactory factory = new TwitterFactory(config);
+               Twitter t = factory.getInstance(AuthorizationFactory
+                               .getInstance(config));
+               // Verify that we can connect!
+               t.getOAuthAccessToken();
+               return t;
+       }
+
+       // TODO: Get secret from credential manager
+       @Override
+       public void dispatch(String messageSubject, String messageContent,
+                       String targetParameter) throws Exception {
+               // messageSubject ignored
+               String[] target = targetParameter.split(":", 2);
+               if (target == null || target.length != 2)
+                       throw new Exception("missing consumer key or secret");
+               String who = target[0];
+               if (!isSendAllowed(who))
+                       return;
+               Twitter twitter = getTwitter(who, target[1]);
+
+               if (messageContent.length() > MAX_MESSAGE_LENGTH)
+                       messageContent = messageContent
+                                       .substring(0, MAX_MESSAGE_LENGTH - 1) + 
ELLIPSIS;
+               twitter.updateStatus(messageContent);
+       }
+
+       @Override
+       public boolean isAvailable() {
+               try {
+                       // Try to create the configuration and push it through 
as far as
+                       // confirming that we can build an access object (even 
if it isn't
+                       // bound to a user)
+                       new TwitterFactory(new 
PropertyConfiguration(getConfig()))
+                                       .getInstance();
+                       return true;
+               } catch (Exception e) {
+                       return false;
+               }
+       }
+
+       /**
+        * Indicates that the dispatcher has not been configured with service
+        * credentials.
+        * 
+        * @author Donal Fellows
+        */
+       @SuppressWarnings("serial")
+       public static class NotConfiguredException extends Exception {
+               NotConfiguredException() {
+                       super("not configured with xAuth key and secret; "
+                                       + "dispatch not possible");
+               }
+       }
+
+       /**
+        * Indicates that the user did not supply their credentials.
+        * 
+        * @author Donal Fellows
+        */
+       @SuppressWarnings("serial")
+       public static class NoCredentialsException extends Exception {
+               NoCredentialsException() {
+                       super("no consumer key and secret present; "
+                                       + "dispatch not possible");
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/AtomFeed.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/AtomFeed.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/AtomFeed.java
new file mode 100644
index 0000000..e5beaeb
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/AtomFeed.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification.atom;
+
+import static java.lang.String.format;
+import static java.util.UUID.randomUUID;
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.common.Uri.secure;
+
+import java.net.URI;
+import java.util.Date;
+
+import javax.annotation.security.RolesAllowed;
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.model.Feed;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.web.context.ServletContextAware;
+import org.taverna.server.master.TavernaServerSupport;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.rest.TavernaServerREST.EventFeed;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * Simple REST handler that allows an Atom feed to be served up of events
+ * generated by workflow runs.
+ * 
+ * @author Donal Fellows
+ */
+public class AtomFeed implements EventFeed, UriBuilderFactory,
+               ServletContextAware {
+       /**
+        * The name of a parameter that states what address we should claim 
that the
+        * feed's internally-generated URIs are relative to. If not set, a 
default
+        * will be guessed.
+        */
+       public static final String PREFERRED_URI_PARAM = 
"taverna.preferredUserUri";
+       private EventDAO eventSource;
+       private TavernaServerSupport support;
+       private URI baseURI;
+       private Abdera abdera;
+       private String feedLanguage = "en";
+       private String uuid = randomUUID().toString();
+
+       @Required
+       public void setEventSource(EventDAO eventSource) {
+               this.eventSource = eventSource;
+       }
+
+       @Required
+       public void setSupport(TavernaServerSupport support) {
+               this.support = support;
+       }
+
+       public void setFeedLanguage(String language) {
+               this.feedLanguage = language;
+       }
+
+       public String getFeedLanguage() {
+               return feedLanguage;
+       }
+
+       @Required
+       public void setAbdera(Abdera abdera) {
+               this.abdera = abdera;
+       }
+
+       @Override
+       @CallCounted
+       @RolesAllowed(USER)
+       public Feed getFeed(UriInfo ui) {
+               Feed feed = abdera.getFactory().newFeed();
+               feed.setTitle("events relating to workflow runs").setLanguage(
+                               feedLanguage);
+               String user = support.getPrincipal().toString()
+                               .replaceAll("[^A-Za-z0-9]+", "");
+               feed.setId(format("urn:taverna-server:%s:%s", uuid, user));
+               org.joda.time.DateTime modification = null;
+               for (Event e : eventSource.getEvents(support.getPrincipal())) {
+                       if (modification == null || 
e.getPublished().isAfter(modification))
+                               modification = e.getPublished();
+                       feed.addEntry(e.getEntry(abdera, feedLanguage));
+               }
+               if (modification == null)
+                       feed.setUpdated(new Date());
+               else
+                       feed.setUpdated(modification.toDate());
+               feed.addLink(ui.getAbsolutePath().toASCIIString(), "self");
+               return feed;
+       }
+
+       @Override
+       @CallCounted
+       @RolesAllowed(USER)
+       public Entry getEvent(String id) {
+               return eventSource.getEvent(support.getPrincipal(), 
id).getEntry(
+                               abdera, feedLanguage);
+       }
+
+       @Override
+       public UriBuilder getRunUriBuilder(TavernaRun run) {
+               return 
secure(fromUri(getBaseUriBuilder().path("runs/{uuid}").build(
+                               run.getId())));
+       }
+
+       @Override
+       public UriBuilder getBaseUriBuilder() {
+               return secure(fromUri(baseURI));
+       }
+
+       @Override
+       public String resolve(String uri) {
+               if (uri == null)
+                       return null;
+               return secure(baseURI, uri).toString();
+       }
+
+       @Override
+       public void setServletContext(ServletContext servletContext) {
+               String base = 
servletContext.getInitParameter(PREFERRED_URI_PARAM);
+               if (base == null)
+                       base = servletContext.getContextPath() + "/rest";
+               baseURI = URI.create(base);
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/Event.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/Event.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/Event.java
new file mode 100644
index 0000000..825029d
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/Event.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification.atom;
+
+import static java.util.UUID.randomUUID;
+
+import java.io.Serializable;
+import java.net.URI;
+import java.util.Date;
+
+import javax.jdo.annotations.Column;
+import javax.jdo.annotations.Index;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.Queries;
+import javax.jdo.annotations.Query;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.model.Entry;
+import org.joda.time.DateTime;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Parent class of all events that may appear on the feed for a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+@SuppressWarnings("serial")
+@PersistenceCapable(schema = "ATOM", table = "EVENTS")
+@Queries({
+               @Query(name = "eventsForUser", language = "SQL", value = 
"SELECT id FROM ATOM.EVENTS WHERE owner = ? ORDER BY published DESC", 
resultClass = String.class),
+               @Query(name = "eventForUserAndId", language = "SQL", value = 
"SELECT id FROM ATOM.EVENTS WHERE owner = ? AND id = ?", resultClass = 
String.class),
+               @Query(name = "eventsFromBefore", language = "SQL", value = 
"SELECT id FROM ATOM.EVENTS where published < ?", resultClass = String.class) })
+public class Event implements Serializable {
+       @Persistent(primaryKey = "true")
+       @Column(length = 48)
+       private String id;
+       @Persistent
+       private String owner;
+       @Persistent
+       @Index
+       private Date published;
+       @Persistent
+       private String message;
+       @Persistent
+       private String title;
+       @Persistent
+       private String link;
+
+       Event() {
+       }
+
+       /**
+        * Initialise the identity of this event and the point at which it was
+        * published.
+        * 
+        * @param idPrefix
+        *            A prefix for the identity of this event.
+        * @param owner
+        *            Who is the owner of this event.
+        */
+       Event(String idPrefix, URI workflowLink, UsernamePrincipal owner,
+                       String title, String message) {
+               id = idPrefix + "." + randomUUID().toString();
+               published = new Date();
+               this.owner = owner.getName();
+               this.title = title;
+               this.message = message;
+               this.link = workflowLink.toASCIIString();
+       }
+
+       public final String getId() {
+               return id;
+       }
+
+       public final String getOwner() {
+               return owner;
+       }
+
+       public final DateTime getPublished() {
+               return new DateTime(published);
+       }
+
+       public String getMessage() {
+               return message;
+       }
+
+       public String getTitle() {
+               return title;
+       }
+
+       public String getLink() {
+               return link;
+       }
+
+       public Entry getEntry(Abdera abdera, String language) {
+               Entry entry = abdera.getFactory().newEntry();
+               entry.setId(id);
+               entry.setPublished(published);
+               entry.addAuthor(owner).setLanguage(language);
+               entry.setUpdated(published);
+               entry.setTitle(title).setLanguage(language);
+               entry.addLink(link, "related").setTitle("workflow run");
+               entry.setContent(message).setLanguage(language);
+               return entry;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/EventDAO.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/EventDAO.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/EventDAO.java
new file mode 100644
index 0000000..56f25ff
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/EventDAO.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification.atom;
+
+import static java.lang.Thread.interrupted;
+import static java.lang.Thread.sleep;
+import static java.util.Arrays.asList;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PreDestroy;
+import javax.jdo.annotations.PersistenceAware;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.joda.time.DateTime;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.interfaces.MessageDispatcher;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.utils.JDOSupport;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * The database interface that supports the event feed.
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceAware
+public class EventDAO extends JDOSupport<Event> implements MessageDispatcher {
+       public EventDAO() {
+               super(Event.class);
+       }
+
+       @Override
+       public String getName() {
+               return "atom";
+       }
+
+       private Log log = LogFactory.getLog("Taverna.Server.Atom");
+       private UriBuilderFactory ubf;
+       private int expiryAgeDays;
+
+       @Required
+       public void setExpiryAgeDays(int expiryAgeDays) {
+               this.expiryAgeDays = expiryAgeDays;
+       }
+
+       @Required
+       public void setUriBuilderFactory(UriBuilderFactory ubf) {
+               this.ubf = ubf;
+       }
+
+       /**
+        * Get the given user's list of events.
+        * 
+        * @param user
+        *            The identity of the user to get the events for.
+        * @return A copy of the list of events currently known about.
+        */
+       @Nonnull
+       @WithinSingleTransaction
+       public List<Event> getEvents(@Nonnull UsernamePrincipal user) {
+               @SuppressWarnings("unchecked")
+               List<String> ids = (List<String>) 
namedQuery("eventsForUser").execute(
+                               user.getName());
+               if (log.isDebugEnabled())
+                       log.debug("found " + ids.size() + " events for user " + 
user);
+
+               List<Event> result = new ArrayList<>();
+               for (String id : ids) {
+                       Event event = getById(id);
+                       result.add(detach(event));
+               }
+               return result;
+       }
+
+       /**
+        * Get a particular event.
+        * 
+        * @param user
+        *            The identity of the user to get the event for.
+        * @param id
+        *            The handle of the event to look up.
+        * @return A copy of the event.
+        */
+       @Nonnull
+       @WithinSingleTransaction
+       public Event getEvent(@Nonnull UsernamePrincipal user, @Nonnull String 
id) {
+               @SuppressWarnings("unchecked")
+               List<String> ids = (List<String>) 
namedQuery("eventForUserAndId")
+                               .execute(user.getName(), id);
+               if (log.isDebugEnabled())
+                       log.debug("found " + ids.size() + " events for user " + 
user
+                                       + " with id = " + id);
+
+               if (ids.size() != 1)
+                       throw new IllegalArgumentException("no such id");
+               return detach(getById(ids.get(0)));
+       }
+
+       /**
+        * Delete a particular event.
+        * 
+        * @param id
+        *            The identifier of the event to delete.
+        */
+       @WithinSingleTransaction
+       public void deleteEventById(@Nonnull String id) {
+               delete(getById(id));
+       }
+
+       /**
+        * Delete all events that have expired.
+        */
+       @WithinSingleTransaction
+       public void deleteExpiredEvents() {
+               Date death = new DateTime().plusDays(-expiryAgeDays).toDate();
+               death = new Timestamp(death.getTime()); // UGLY SQL HACK
+
+               @SuppressWarnings("unchecked")
+               List<String> ids = (List<String>) namedQuery("eventsFromBefore")
+                               .execute(death);
+               if (log.isDebugEnabled() && !ids.isEmpty())
+                       log.debug("found " + ids.size()
+                                       + " events to be squelched (older than 
" + death + ")");
+
+               for (String id : ids)
+                       delete(getById(id));
+       }
+
+       @Override
+       public boolean isAvailable() {
+               return true;
+       }
+
+       private BlockingQueue<Event> insertQueue = new ArrayBlockingQueue<>(16);
+
+       @Override
+       public void dispatch(TavernaRun originator, String messageSubject,
+                       String messageContent, String targetParameter) throws 
Exception {
+               insertQueue.put(new Event("finish", 
ubf.getRunUriBuilder(originator)
+                               .build(), 
originator.getSecurityContext().getOwner(),
+                               messageSubject, messageContent));
+       }
+
+       public void started(TavernaRun originator, String messageSubject,
+                       String messageContent) throws InterruptedException {
+               insertQueue.put(new Event("start", 
ubf.getRunUriBuilder(originator)
+                               .build(), 
originator.getSecurityContext().getOwner(),
+                               messageSubject, messageContent));
+       }
+
+       private Thread eventDaemon;
+       private boolean shuttingDown = false;
+
+       @Required
+       public void setSelf(final EventDAO dao) {
+               eventDaemon = new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       while (!shuttingDown && !interrupted()) 
{
+                                               transferEvents(dao, new 
ArrayList<Event>(
+                                                               
asList(insertQueue.take())));
+                                               sleep(5000);
+                                       }
+                               } catch (InterruptedException e) {
+                               } finally {
+                                       transferEvents(dao, new 
ArrayList<Event>());
+                               }
+                       }
+               }, "ATOM event daemon");
+               eventDaemon.setContextClassLoader(null);
+               eventDaemon.setDaemon(true);
+               eventDaemon.start();
+       }
+
+       private void transferEvents(EventDAO dao, List<Event> e) {
+               insertQueue.drainTo(e);
+               dao.storeEvents(e);
+       }
+
+       @PreDestroy
+       void stopDaemon() {
+               shuttingDown = true;
+               if (eventDaemon != null)
+                       eventDaemon.interrupt();
+       }
+
+       @WithinSingleTransaction
+       protected void storeEvents(List<Event> events) {
+               for (Event e : events)
+                       persist(e);
+               log.info("stored " + events.size() + " notification events");
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/package-info.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/package-info.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/package-info.java
new file mode 100644
index 0000000..9cc592d
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/atom/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the Atom feed implementation within Taverna Server.
+ * @author Donal Fellows
+ */
+@XmlSchema(namespace = FEED, elementFormDefault = QUALIFIED, 
attributeFormDefault = QUALIFIED, xmlns = {
+               @XmlNs(prefix = "xlink", namespaceURI = XLINK),
+               @XmlNs(prefix = "ts", namespaceURI = SERVER),
+               @XmlNs(prefix = "ts-rest", namespaceURI = SERVER_REST),
+               @XmlNs(prefix = "ts-soap", namespaceURI = SERVER_SOAP),
+               @XmlNs(prefix = "feed", namespaceURI = FEED),
+               @XmlNs(prefix = "admin", namespaceURI = ADMIN) })
+package org.taverna.server.master.notification.atom;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.master.common.Namespaces.ADMIN;
+import static org.taverna.server.master.common.Namespaces.FEED;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.SERVER_REST;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/package-info.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/package-info.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/package-info.java
new file mode 100644
index 0000000..979066a
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/notification/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * The notification fabric and implementations of notification dispatchers
+ * that support subscription.
+ */
+package org.taverna.server.master.notification;

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/package-info.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/package-info.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/package-info.java
new file mode 100644
index 0000000..17bfd03
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/package-info.java
@@ -0,0 +1,11 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * The core of the implementation of Taverna Server, including the
+ * implementations of the SOAP and REST interfaces.
+ */
+package org.taverna.server.master;
+

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/ContentTypes.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/ContentTypes.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/ContentTypes.java
new file mode 100644
index 0000000..b0819a5
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/ContentTypes.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_ATOM_XML;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
+import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+
+/**
+ * Miscellaneous content type constants.
+ * 
+ * @author Donal Fellows
+ */
+interface ContentTypes {
+       static final String URI_LIST = "text/uri-list";
+       static final String ZIP = "application/zip";
+       static final String TEXT = TEXT_PLAIN;
+       static final String XML = APPLICATION_XML;
+       static final String JSON = APPLICATION_JSON;
+       static final String BYTES = APPLICATION_OCTET_STREAM;
+       static final String ATOM = APPLICATION_ATOM_XML;
+       static final String ROBUNDLE = "application/vnd.wf4ever.robundle+zip";
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/DirectoryContents.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/DirectoryContents.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/DirectoryContents.java
new file mode 100644
index 0000000..e01d1c4
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/DirectoryContents.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static org.taverna.server.master.common.Uri.secure;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlElementRef;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+
+import org.taverna.server.master.common.DirEntryReference;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+
+/**
+ * The result of a RESTful operation to list the contents of a directory. Done
+ * with JAXB.
+ * 
+ * @author Donal Fellows
+ */
+@XmlRootElement
+@XmlType(name = "DirectoryContents")
+@XmlSeeAlso(MakeOrUpdateDirEntry.class)
+public class DirectoryContents {
+       /**
+        * The contents of the directory.
+        */
+       @XmlElementRef
+       public List<DirEntryReference> contents;
+
+       /**
+        * Make an empty directory description. Required for JAXB.
+        */
+       public DirectoryContents() {
+               contents = new ArrayList<>();
+       }
+
+       /**
+        * Make a directory description.
+        * 
+        * @param ui
+        *            The factory for URIs.
+        * @param collection
+        *            The real directory contents that we are to describe.
+        */
+       public DirectoryContents(UriInfo ui, Collection<DirectoryEntry> 
collection) {
+               contents = new ArrayList<>();
+               UriBuilder ub = secure(ui).path("{filename}");
+               for (DirectoryEntry e : collection)
+                       contents.add(DirEntryReference.newInstance(ub, e));
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/FileSegment.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/FileSegment.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/FileSegment.java
new file mode 100644
index 0000000..74269c1
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/FileSegment.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static javax.ws.rs.core.Response.ok;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+/**
+ * Representation of a segment of a file to be read by JAX-RS.
+ * 
+ * @author Donal Fellows
+ */
+public class FileSegment {
+       /** The file to read a segment of. */
+       public final File file;
+       /** The offset of the first byte of the segment to read. */
+       public Integer from;
+       /** The offset of the first byte after the segment to read. */
+       public Integer to;
+
+       /**
+        * Parse the HTTP Range header and determine what exact range of the 
file to
+        * read.
+        * 
+        * @param f
+        *            The file this refers to
+        * @param range
+        *            The content of the Range header.
+        * @throws FilesystemAccessException
+        *             If we can't determine the length of the file (shouldn't
+        *             happen).
+        */
+       public FileSegment(File f, String range) throws 
FilesystemAccessException {
+               file = f;
+               Matcher m = 
Pattern.compile("^\\s*bytes=(\\d*)-(\\d*)\\s*$").matcher(
+                               range);
+               if (m.matches()) {
+                       if (!m.group(1).isEmpty())
+                               from = Integer.valueOf(m.group(1));
+                       if (!m.group(2).isEmpty())
+                               to = Integer.valueOf(m.group(2)) + 1;
+                       int size = (int) f.getSize();
+                       if (from == null) {
+                               from = size - to;
+                               to = size;
+                       } else if (to == null)
+                               to = size;
+                       else if (to > size)
+                               to = size;
+               }
+       }
+
+       /**
+        * Convert to a response, as per RFC 2616.
+        * 
+        * @param type
+        *            The expected type of the data.
+        * @return A JAX-RS response.
+        */
+       public Response toResponse(MediaType type) {
+               if (from == null && to == null)
+                       return ok(file).type(type).build();
+               if (from >= to)
+                       return ok("Requested range not 
satisfiable").status(416).build();
+               return ok(this).status(206).type(type).build();
+       }
+}
\ 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/rest/InteractionFeedREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/InteractionFeedREST.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/InteractionFeedREST.java
new file mode 100644
index 0000000..b9b4718
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/InteractionFeedREST.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static org.taverna.server.master.rest.ContentTypes.ATOM;
+
+import java.net.MalformedURLException;
+
+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.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.model.Feed;
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+
+/**
+ * A very stripped down ATOM feed for the interaction service.
+ * 
+ * @author Donal Fellows
+ */
+public interface InteractionFeedREST {
+       /**
+        * Get the feed document for this ATOM feed.
+        * 
+        * @return The feed.
+        * @throws FilesystemAccessException
+        *             If we can't read from the feed directory.
+        * @throws NoDirectoryEntryException
+        *             If something changes things under our feet.
+        */
+       @GET
+       @Path("/")
+       @Produces(ATOM)
+       @Description("Get the feed document for this ATOM feed.")
+       Feed getFeed() throws FilesystemAccessException, 
NoDirectoryEntryException;
+
+       /**
+        * Adds an entry to this ATOM feed.
+        * 
+        * @param entry
+        *            The entry to create.
+        * @return A redirect to the created entry.
+        * @throws MalformedURLException
+        *             If we have problems generating the URI of the entry.
+        * @throws FilesystemAccessException
+        *             If we can't create the feed entry file.
+        * @throws NoDirectoryEntryException
+        *             If things get changed under our feet.
+        * @throws NoUpdateException
+        *             If we don't have permission to change things relating to 
this
+        *             run.
+        */
+       @POST
+       @Path("/")
+       @Consumes(ATOM)
+       @Produces(ATOM)
+       @Description("Adds an entry to this ATOM feed.")
+       Response addEntry(Entry entry) throws MalformedURLException,
+                       FilesystemAccessException, NoDirectoryEntryException,
+                       NoUpdateException;
+
+       /** Handles the OPTIONS request. */
+       @OPTIONS
+       @Path("/")
+       @Description("Describes what HTTP operations are supported on the 
feed.")
+       Response feedOptions();
+
+       /**
+        * Gets the content of an entry in this ATOM feed.
+        * 
+        * @param id
+        *            The ID of the entry to fetch.
+        * @return The entry contents.
+        * @throws FilesystemAccessException
+        *             If we have problems reading the entry.
+        * @throws NoDirectoryEntryException
+        *             If we can't find the entry to read.
+        */
+       @GET
+       @Path("{id}")
+       @Produces(ATOM)
+       @Description("Get the entry with a particular ID within this ATOM 
feed.")
+       Entry getEntry(@PathParam("id") String id)
+                       throws FilesystemAccessException, 
NoDirectoryEntryException;
+
+       /**
+        * Delete an entry from this ATOM feed.
+        * 
+        * @param id
+        *            The ID of the entry to delete.
+        * @return A simple message. Not very important!
+        * @throws FilesystemAccessException
+        *             If we have problems deleting the entry.
+        * @throws NoDirectoryEntryException
+        *             If we can't find the entry to delete.
+        * @throws NoUpdateException
+        *             If we don't have permission to alter things relating to 
this
+        *             run.
+        */
+       @DELETE
+       @Path("{id}")
+       @Produces("text/plain")
+       @Description("Deletes an entry from this ATOM feed.")
+       String deleteEntry(@PathParam("id") String id)
+                       throws FilesystemAccessException, 
NoDirectoryEntryException,
+                       NoUpdateException;
+
+       /** Handles the OPTIONS request. */
+       @OPTIONS
+       @Path("{id}")
+       @Description("Describes what HTTP operations are supported on an 
entry.")
+       Response entryOptions(@PathParam("{id}") String id);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/ListenerDefinition.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/ListenerDefinition.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/ListenerDefinition.java
new file mode 100644
index 0000000..7a072be
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/ListenerDefinition.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+/**
+ * Description of what sort of event listener to create and attach to a 
workflow
+ * run. Bound via JAXB.
+ * 
+ * @author Donal Fellows
+ */
+@XmlRootElement(name = "listenerDefinition")
+@XmlType(name="ListenerDefinition")
+public class ListenerDefinition {
+       /**
+        * The type of event listener to create.
+        */
+       @XmlAttribute
+       public String type;
+       /**
+        * How the event listener should be configured.
+        */
+       @XmlValue
+       public String configuration;
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/MakeOrUpdateDirEntry.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/MakeOrUpdateDirEntry.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/MakeOrUpdateDirEntry.java
new file mode 100644
index 0000000..0138f88
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/MakeOrUpdateDirEntry.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+/**
+ * The input to the REST interface for making directories and files, and
+ * uploading file contents. Done with JAXB.
+ * 
+ * @author Donal Fellows
+ */
+@XmlRootElement(name = "filesystemOperation")
+@XmlType(name = "FilesystemCreationOperation")
+@XmlSeeAlso( { MakeOrUpdateDirEntry.MakeDirectory.class,
+               MakeOrUpdateDirEntry.SetFileContents.class })
+public abstract class MakeOrUpdateDirEntry {
+       /**
+        * The name of the file or directory that the operation applies to.
+        */
+       @XmlAttribute
+       public String name;
+       /**
+        * The contents of the file to upload.
+        */
+       @XmlValue
+       public byte[] contents;
+
+       /**
+        * Create a directory, described with JAXB. Should leave the
+        * {@link MakeOrUpdateDirEntry#contents contents} field empty.
+        * 
+        * @author Donal Fellows
+        */
+       @XmlRootElement(name = "mkdir")
+       @XmlType(name = "MakeDirectory")
+       public static class MakeDirectory extends MakeOrUpdateDirEntry {
+       }
+
+       /**
+        * Create a file or set its contents, described with JAXB.
+        * 
+        * @author Donal Fellows
+        */
+       @XmlRootElement(name = "upload")
+       @XmlType(name = "UploadFile")
+       public static class SetFileContents extends MakeOrUpdateDirEntry {
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerDirectoryREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerDirectoryREST.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerDirectoryREST.java
new file mode 100644
index 0000000..ea2f776
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerDirectoryREST.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static java.util.Collections.unmodifiableList;
+import static javax.ws.rs.core.MediaType.WILDCARD;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.ContentTypes.BYTES;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.URI_LIST;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+import static org.taverna.server.master.rest.ContentTypes.ZIP;
+
+import java.io.InputStream;
+import java.net.URI;
+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.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.HttpHeaders;
+import javax.ws.rs.core.PathSegment;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.core.Variant;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+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.File;
+
+/**
+ * Representation of how a workflow run's working directory tree looks.
+ * 
+ * @author Donal Fellows
+ */
+@RolesAllowed(USER)
+@Produces({ XML, JSON })
+@Consumes({ XML, JSON })
+@Description("Representation of how a workflow run's working directory tree 
looks.")
+public interface TavernaServerDirectoryREST {
+       /**
+        * Get the working directory of the workflow run.
+        * 
+        * @param ui
+        *            About how this method was called.
+        * @return A description of the working directory.
+        * @throws FilesystemAccessException
+        */
+       @GET
+       @Path("/")
+       @Description("Describes the working directory of the workflow run.")
+       @Nonnull
+       DirectoryContents getDescription(@Nonnull @Context UriInfo ui)
+                       throws FilesystemAccessException;
+
+       /** Get an outline of the operations supported. */
+       @OPTIONS
+       @Path("{path:.*}")
+       @Description("Produces the description of the files/directories' 
baclava operations.")
+       Response options(@PathParam("path") List<PathSegment> path);
+
+       /**
+        * Gets a description of the named entity in or beneath the working
+        * directory of the workflow run, which may be either a {@link 
Directory} or
+        * a {@link File}.
+        * 
+        * @param path
+        *            The path to the thing to describe.
+        * @param ui
+        *            About how this method was called.
+        * @param headers
+        *            About what the caller was looking for.
+        * @return An HTTP response containing a description of the named thing.
+        * @throws NoDirectoryEntryException
+        *             If the name of the file or directory can't be looked up.
+        * @throws FilesystemAccessException
+        *             If something went wrong during the filesystem operation.
+        * @throws NegotiationFailedException
+        *             If the content type being downloaded isn't one that this
+        *             method can support.
+        */
+       @GET
+       @Path("{path:.+}")
+       @Produces({ XML, JSON, BYTES, ZIP, WILDCARD })
+       @Description("Gives a description of the named entity in or beneath the 
"
+                       + "working directory of the workflow run (either a 
Directory or File).")
+       @Nonnull
+       Response getDirectoryOrFileContents(
+                       @Nonnull @PathParam("path") List<PathSegment> path,
+                       @Nonnull @Context UriInfo ui, @Nonnull @Context 
HttpHeaders headers)
+                       throws NoDirectoryEntryException, 
FilesystemAccessException,
+                       NegotiationFailedException;
+
+       /**
+        * Creates a directory in the filesystem beneath the working directory 
of
+        * the workflow run, or creates or updates a file's contents, where that
+        * file is in or below the working directory of a workflow run.
+        * 
+        * @param parent
+        *            The directory to create the directory in.
+        * @param operation
+        *            What to call the directory to create.
+        * @param ui
+        *            About how this method was called.
+        * @return An HTTP response indicating where the directory was actually 
made
+        *         or what file was created/updated.
+        * @throws NoDirectoryEntryException
+        *             If the name of the containing directory can't be looked 
up.
+        * @throws NoUpdateException
+        *             If the user is not permitted to update the run.
+        * @throws FilesystemAccessException
+        *             If something went wrong during the filesystem operation.
+        */
+       @POST
+       @Path("{path:.*}")
+       @Description("Creates a directory in the filesystem beneath the working 
"
+                       + "directory of the workflow run, or creates or updates 
a file's "
+                       + "contents, where that file is in or below the working 
directory "
+                       + "of a workflow run.")
+       @Nonnull
+       Response makeDirectoryOrUpdateFile(
+                       @Nonnull @PathParam("path") List<PathSegment> parent,
+                       @Nonnull MakeOrUpdateDirEntry operation,
+                       @Nonnull @Context UriInfo ui) throws NoUpdateException,
+                       FilesystemAccessException, NoDirectoryEntryException;
+
+       /**
+        * Creates or updates a file in a particular location beneath the 
working
+        * directory of the workflow run.
+        * 
+        * @param file
+        *            The path to the file to create or update.
+        * @param referenceList
+        *            Location to get the file's contents from. Must be
+        *            <i>publicly</i> readable.
+        * @param ui
+        *            About how this method was called.
+        * @return An HTTP response indicating what file was created/updated.
+        * @throws NoDirectoryEntryException
+        *             If the name of the containing directory can't be looked 
up.
+        * @throws NoUpdateException
+        *             If the user is not permitted to update the run.
+        * @throws FilesystemAccessException
+        *             If something went wrong during the filesystem operation.
+        */
+       @POST
+       @Path("{path:(.*)}")
+       @Consumes(URI_LIST)
+       @Description("Creates or updates a file in a particular location 
beneath the "
+                       + "working directory of the workflow run with the 
contents of a "
+                       + "publicly readable URL.")
+       @Nonnull
+       Response setFileContentsFromURL(@PathParam("path") List<PathSegment> 
file,
+                       List<URI> referenceList, @Context UriInfo ui)
+                       throws NoDirectoryEntryException, NoUpdateException,
+                       FilesystemAccessException;
+
+    /**
+        * Creates or updates a file in a particular location beneath the 
working
+        * directory of the workflow run.
+        * 
+        * @param file
+        *            The path to the file to create or update.
+        * @param contents
+        *            Stream of bytes to set the file's contents to.
+        * @param ui
+        *            About how this method was called.
+        * @return An HTTP response indicating what file was created/updated.
+        * @throws NoDirectoryEntryException
+        *             If the name of the containing directory can't be looked 
up.
+        * @throws NoUpdateException
+        *             If the user is not permitted to update the run.
+        * @throws FilesystemAccessException
+        *             If something went wrong during the filesystem operation.
+        */
+       @PUT
+       @Path("{path:(.*)}")
+       @Consumes({ BYTES, WILDCARD })
+       @Description("Creates or updates a file in a particular location 
beneath the "
+                       + "working directory of the workflow run.")
+       @Nonnull
+       Response setFileContents(@PathParam("path") List<PathSegment> file,
+                       InputStream contents, @Context UriInfo ui)
+                       throws NoDirectoryEntryException, NoUpdateException,
+                       FilesystemAccessException;
+
+       /**
+        * Deletes a file or directory that is in or below the working 
directory of
+        * a workflow run.
+        * 
+        * @param path
+        *            The path to the file or directory.
+        * @return An HTTP response to the method.
+        * @throws NoUpdateException
+        *             If the user is not permitted to update the run.
+        * @throws FilesystemAccessException
+        *             If something went wrong during the filesystem operation.
+        * @throws NoDirectoryEntryException
+        *             If the name of the file or directory can't be looked up.
+        */
+       @DELETE
+       @Path("{path:.*}")
+       @Description("Deletes a file or directory that is in or below the 
working "
+                       + "directory of a workflow run.")
+       @Nonnull
+       Response destroyDirectoryEntry(@PathParam("path") List<PathSegment> 
path)
+                       throws NoUpdateException, FilesystemAccessException,
+                       NoDirectoryEntryException;
+
+       /**
+        * Exception thrown to indicate a failure by the client to provide an
+        * acceptable content type.
+        * 
+        * @author Donal Fellows
+        */
+       @SuppressWarnings("serial")
+       public static class NegotiationFailedException extends Exception {
+               public List<Variant> accepted;
+
+               public NegotiationFailedException(String msg, List<Variant> 
accepted) {
+                       super(msg);
+                       this.accepted = unmodifiableList(accepted);
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerInputREST.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerInputREST.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerInputREST.java
new file mode 100644
index 0000000..faed19d
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerInputREST.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+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.TavernaServerInputREST.PathNames.BACLAVA;
+import static 
org.taverna.server.master.rest.TavernaServerInputREST.PathNames.EXPECTED;
+import static 
org.taverna.server.master.rest.TavernaServerInputREST.PathNames.ONE_INPUT;
+
+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.GET;
+import javax.ws.rs.OPTIONS;
+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.PathSegment;
+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.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+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.NoUpdateException;
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.port_description.InputDescription;
+
+/**
+ * This represents how a Taverna Server workflow run's inputs looks to a 
RESTful
+ * API.
+ * 
+ * @author Donal Fellows.
+ */
+@RolesAllowed(USER)
+@Description("This represents how a Taverna Server workflow run's inputs "
+               + "looks to a RESTful API.")
+public interface TavernaServerInputREST {
+       /**
+        * @return A description of the various URIs to inputs associated with a
+        *         workflow run.
+        */
+       @GET
+       @Path("/")
+       @Produces({ XML, JSON })
+       @Description("Describe the sub-URIs of this resource.")
+       @Nonnull
+       InputsDescriptor get();
+
+       /** Get an outline of the operations supported. */
+       @OPTIONS
+       @Path("/")
+       @Description("Produces the description of one run's inputs' 
operations.")
+       Response options();
+
+       /**
+        * @return A description of the various URIs to inputs associated with a
+        *         workflow run.
+        */
+       @GET
+       @Path(EXPECTED)
+       @Produces({ XML, JSON })
+       @Description("Describe the expected inputs of this workflow run.")
+       @Nonnull
+       InputDescription getExpected();
+
+       /** Get an outline of the operations supported. */
+       @OPTIONS
+       @Path(EXPECTED)
+       @Description("Produces the description of the expected inputs' 
operations.")
+       Response expectedOptions();
+
+       /**
+        * @return The Baclava file that will supply all the inputs to the 
workflow
+        *         run, or empty to indicate that no such file is specified.
+        */
+       @GET
+       @Path(BACLAVA)
+       @Produces(TEXT)
+       @Description("Gives the Baclava file describing the inputs, or empty if 
"
+                       + "individual files are used.")
+       @Nonnull
+       String getBaclavaFile();
+
+       /**
+        * Set the Baclava file that will supply all the inputs to the workflow 
run.
+        * 
+        * @param filename
+        *            The filename to set.
+        * @return The name of the Baclava file that was actually set.
+        * @throws NoUpdateException
+        *             If the user can't update the run.
+        * @throws BadStateChangeException
+        *             If the run is not Initialized.
+        * @throws FilesystemAccessException
+        *             If the filename starts with a <tt>/</tt> or if it 
contains a
+        *             <tt>..</tt> segment.
+        */
+       @PUT
+       @Path(BACLAVA)
+       @Consumes(TEXT)
+       @Produces(TEXT)
+       @Description("Sets the Baclava file describing the inputs.")
+       @Nonnull
+       String setBaclavaFile(@Nonnull String filename) throws 
NoUpdateException,
+                       BadStateChangeException, FilesystemAccessException;
+
+       /** Get an outline of the operations supported. */
+       @OPTIONS
+       @Path(BACLAVA)
+       @Description("Produces the description of the inputs' baclava 
operations.")
+       Response baclavaOptions();
+
+       /**
+        * Get what input is set for the specific input.
+        * 
+        * @param name
+        *            The input to set.
+        * @param uriInfo
+        *            About the URI used to access this resource.
+        * @return A description of the input.
+        * @throws BadInputPortNameException
+        *             If no input with that name exists.
+        */
+       @GET
+       @Path(ONE_INPUT)
+       @Produces({ XML, JSON })
+       @Description("Gives a description of what is used to supply a 
particular "
+                       + "input.")
+       @Nonnull
+       InDesc getInput(@Nonnull @PathParam("name") String name,
+                       @Context UriInfo uriInfo) throws 
BadInputPortNameException;
+
+       /**
+        * Set what an input uses to provide data into the workflow run.
+        * 
+        * @param name
+        *            The name of the input.
+        * @param inputDescriptor
+        *            A description of the input
+        * @param uriInfo
+        *            About the URI used to access this resource.
+        * @return A description of the input.
+        * @throws NoUpdateException
+        *             If the user can't update the run.
+        * @throws BadStateChangeException
+        *             If the run is not Initialized.
+        * @throws FilesystemAccessException
+        *             If a filename is being set and the filename starts with a
+        *             <tt>/</tt> or if it contains a <tt>..</tt> segment.
+        * @throws BadInputPortNameException
+        *             If no input with that name exists.
+        * @throws BadPropertyValueException
+        *             If some bad misconfiguration has happened.
+        */
+       @PUT
+       @Path(ONE_INPUT)
+       @Consumes({ XML, JSON })
+       @Produces({ XML, JSON })
+       @Description("Sets the source for a particular input port.")
+       @Nonnull
+       InDesc setInput(@Nonnull @PathParam("name") String name,
+                       @Nonnull InDesc inputDescriptor, @Context UriInfo 
uriInfo) throws NoUpdateException,
+                       BadStateChangeException, FilesystemAccessException,
+                       BadPropertyValueException, BadInputPortNameException;
+
+       /** Get an outline of the operations supported. */
+       @OPTIONS
+       @Path(ONE_INPUT)
+       @Description("Produces the description of the one input's operations.")
+       Response inputOptions(@PathParam("name") String name);
+
+       interface PathNames {
+               final String EXPECTED = "expected";
+               final String BACLAVA = "baclava";
+               final String ONE_INPUT = "input/{name}";
+       }
+
+       /**
+        * A description of the structure of inputs to a Taverna workflow run, 
done
+        * with JAXB.
+        * 
+        * @author Donal Fellows
+        */
+       @XmlRootElement(name = "runInputs")
+       @XmlType(name = "TavernaRunInputs")
+       public static class InputsDescriptor extends VersionedElement {
+               /**
+                * Where to find a description of the expected inputs to this 
workflow
+                * run.
+                */
+               public Uri expected;
+               /**
+                * Where to find the overall Baclava document filename (if set).
+                */
+               public Uri baclava;
+               /**
+                * Where to find the details of inputs to particular ports (if 
set).
+                */
+               public List<Uri> input;
+
+               /**
+                * Make a blank description of the inputs.
+                */
+               public InputsDescriptor() {
+               }
+
+               /**
+                * Make the description of the inputs.
+                * 
+                * @param ui
+                *            Information about the URIs to generate.
+                * @param run
+                *            The run whose inputs are to be described.
+                */
+               public InputsDescriptor(UriInfo ui, TavernaRun run) {
+                       super(true);
+                       expected = new Uri(ui, EXPECTED);
+                       baclava = new Uri(ui, BACLAVA);
+                       input = new ArrayList<>();
+                       for (Input i : run.getInputs())
+                               input.add(new Uri(ui, ONE_INPUT, i.getName()));
+               }
+       }
+
+       /**
+        * The Details of a particular input port's value assignment, done with
+        * JAXB.
+        * 
+        * @author Donal Fellows
+        */
+       @XmlRootElement(name = "runInput")
+       @XmlType(name = "InputDescription")
+       public static class InDesc extends VersionedElement {
+               /** Make a blank description of an input port. */
+               public InDesc() {
+               }
+
+               /**
+                * Make a description of the given input port.
+                * 
+                * @param inputPort
+                */
+               public InDesc(Input inputPort, UriInfo ui) {
+                       super(true);
+                       name = inputPort.getName();
+                       if (inputPort.getFile() != null) {
+                               assignment = new InDesc.File();
+                               assignment.contents = inputPort.getFile();
+                       } else {
+                               assignment = new InDesc.Value();
+                               assignment.contents = inputPort.getValue();
+                       }
+                       // .../runs/{id}/input/input/{name} ->
+                       // .../runs/{id}/input/expected#{name}
+                       UriBuilder ub = ui.getBaseUriBuilder();
+                       List<PathSegment> segments = ui.getPathSegments();
+                       for (PathSegment s : segments.subList(0, 
segments.size() - 2))
+                               ub.segment(s.getPath());
+                       ub.fragment(name);
+                       descriptorRef = new Uri(ub).ref;
+               }
+
+               /** The name of the port. */
+               @XmlAttribute(required = false)
+               public String name;
+               /** Where the port is described. Ignored in user input. */
+               @XmlAttribute(required = false)
+               @XmlSchemaType(name = "anyURI")
+               public URI descriptorRef;
+               /** The character to use to split the input into a list. */
+               @XmlAttribute(name = "listDelimiter", required = false)
+               public String delimiter;
+
+               /**
+                * Either a filename or a literal string, used to provide input 
to a
+                * workflow port.
+                * 
+                * @author Donal Fellows
+                */
+               @XmlType(name = "InputContents")
+               public static abstract class AbstractContents {
+                       /**
+                        * The contents of the description of the input port. 
Meaning not
+                        * defined.
+                        */
+                       @XmlValue
+                       public String contents;
+               };
+
+               /**
+                * The name of a file that provides input to the port. The
+                * {@link AbstractContents#contents contents} field is a 
filename.
+                * 
+                * @author Donal Fellows
+                */
+               @XmlType(name = "")
+               public static class File extends AbstractContents {
+               }
+
+               /**
+                * The literal input to the port. The {@link 
AbstractContents#contents
+                * contents} field is a literal input value.
+                * 
+                * @author Donal Fellows
+                */
+               @XmlType(name = "")
+               public static class Value extends AbstractContents {
+               }
+
+               /**
+                * A reference to a file elsewhere <i>on this server</i>. The
+                * {@link AbstractContents#contents contents} field is a URL to 
the file
+                * (using the RESTful notation).
+                * 
+                * @author Donal Fellows
+                */
+               @XmlType(name = "")
+               public static class Reference extends AbstractContents {
+               }
+
+               /**
+                * The assignment of input values to the port.
+                */
+               @XmlElements({ @XmlElement(name = "file", type = File.class),
+                               @XmlElement(name = "reference", type = 
Reference.class),
+                               @XmlElement(name = "value", type = Value.class) 
})
+               public AbstractContents assignment;
+       }
+}

Reply via email to