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; + } +}
