http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CallTimeLogger.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CallTimeLogger.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CallTimeLogger.java new file mode 100644 index 0000000..a1ac04a --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CallTimeLogger.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import static java.lang.String.format; +import static java.lang.System.nanoTime; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.apache.commons.logging.LogFactory.getLog; +import static org.taverna.server.master.TavernaServer.JMX_ROOT; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apache.commons.logging.Log; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.taverna.server.master.common.version.Version; + +/** + * This class is responsible for timing all invocations of publicly-exposed + * methods of the webapp. It's connected to the webapp through an AspectJ-style + * pointcut that targets a custom annotation. + * + * @author Donal Fellows + */ +@Aspect +@ManagedResource(objectName = JMX_ROOT + "PerformanceMonitor", description = "The performance monitor for Taverna Server " + + Version.JAVA + + ". Writes to application log using the category 'Taverna.Server.Performance'.") +public class CallTimeLogger { + private long threshold = 4000000; + private Log log = getLog("Taverna.Server.Performance"); + + @ManagedAttribute(description = "Threshold beneath which monitored call times are not logged. In nanoseconds.") + public long getThreshold() { + return threshold; + } + + @ManagedAttribute(description = "Threshold beneath which monitored call times are not logged. In nanoseconds.") + public void setThreshold(long threshold) { + this.threshold = threshold; + } + + /** + * The timer for this aspect. The wrapped invocation will be timed, and a + * log message written if the configured threshold is exceeded. + * + * @param call + * The call being wrapped. + * @return The result of the call. + * @throws Throwable + * If anything goes wrong with the wrapped call. + * @see System#nanoTime() + */ + @Around("@annotation(org.taverna.server.master.utils.CallTimeLogger.PerfLogged)") + public Object time(ProceedingJoinPoint call) throws Throwable { + long fore = nanoTime(); + try { + return call.proceed(); + } finally { + long aft = nanoTime(); + long elapsed = aft - fore; + if (elapsed > threshold) + log.info(format("call to %s took %.3fms", call.toShortString(), + elapsed / 1000000.0)); + } + } + + /** + * Mark methods that should be counted by the invocation counter. + * + * @author Donal Fellows + */ + @Retention(RUNTIME) + @Documented + @Target(METHOD) + public static @interface PerfLogged { + } +}
http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CallTimingFilter.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CallTimingFilter.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CallTimingFilter.java new file mode 100644 index 0000000..aead6fa --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CallTimingFilter.java @@ -0,0 +1,65 @@ +/** + * + */ +package org.taverna.server.master.utils; + +import static java.lang.String.format; +import static java.lang.System.nanoTime; +import static org.apache.commons.logging.LogFactory.getLog; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; + +/** + * Logs the time it takes to service HTTP calls into Taverna Server. + * <p> + * This class is currently not used. + * + * @author Donal Fellows + */ +public class CallTimingFilter implements Filter { + private Log log; + private String name; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + log = getLog("Taverna.Server.Performance"); + name = filterConfig.getInitParameter("name"); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (request instanceof HttpServletRequest) + doFilter((HttpServletRequest) request, + (HttpServletResponse) response, chain); + else + chain.doFilter(request, response); + } + + public void doFilter(HttpServletRequest request, + HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + long start = nanoTime(); + chain.doFilter(request, response); + long elapsedTime = nanoTime() - start; + log.info(format("%s call to %s %s took %.3fms", name, + request.getMethod(), request.getRequestURI(), + elapsedTime / 1000000.0)); + } + + @Override + public void destroy() { + log = null; + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CapabilityLister.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CapabilityLister.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CapabilityLister.java new file mode 100644 index 0000000..54b0420 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CapabilityLister.java @@ -0,0 +1,44 @@ +package org.taverna.server.master.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Properties; + +import javax.annotation.PostConstruct; + +import org.taverna.server.master.common.Capability; + +/** + * Utility for listing the capabilities supported by this Taverna Server + * installation. + * + * @author Donal Fellows + */ +public class CapabilityLister { + public static final String CAPABILITY_RESOURCE_FILE = "/capabilities.properties"; + private Properties properties = new Properties(); + + @PostConstruct + void loadCapabilities() throws IOException { + try (InputStream is = getClass().getResourceAsStream( + CAPABILITY_RESOURCE_FILE)) { + if (is != null) + properties.load(is); + } + } + + public List<Capability> getCapabilities() { + List<Capability> caps = new ArrayList<>(); + for (Entry<Object, Object> entry : properties.entrySet()) { + Capability c = new Capability(); + c.capability = URI.create(entry.getKey().toString()); + c.version = entry.getValue().toString(); + caps.add(c); + } + return caps; + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CertificateChainFetcher.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CertificateChainFetcher.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CertificateChainFetcher.java new file mode 100644 index 0000000..c27502f --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/CertificateChainFetcher.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2013 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import java.io.IOException; +import java.net.URI; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import javax.xml.ws.Holder; + +/** + * Obtains the certificate chain for an arbitrary SSL service. Maintains a + * cache. + * + * @author Donal Fellows + */ +public class CertificateChainFetcher { + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getKeystoreType() { + return keystoreType; + } + + public void setKeystoreType(String keystoreType) { + this.keystoreType = keystoreType; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + private boolean secure = true; + private String protocol = "TLS"; + private String keystoreType = KeyStore.getDefaultType(); + private String algorithm = TrustManagerFactory.getDefaultAlgorithm(); + private int timeout = 10000; + + /** + * Get the certificate chain for a service. + * + * @param host + * The host (name or IP address) to contact the service on. + * @param port + * The port to contact the service on. + * @return The certificate chain, or <tt>null</tt> if no credentials are + * available. + * @throws NoSuchAlgorithmException + * If the trust manager cannot be set up because of algorithm + * problems. + * @throws KeyStoreException + * If the trust manager cannot be set up because of problems + * with the keystore type. + * @throws CertificateException + * If a bad certificate is present in the default keystore; + * <i>should be impossible</i>. + * @throws IOException + * If problems happen when trying to contact the service. + * @throws KeyManagementException + * If the SSL context can't have its special context manager + * installed. + */ + private X509Certificate[] getCertificateChainForService(String host, + int port) throws NoSuchAlgorithmException, KeyStoreException, + CertificateException, IOException, KeyManagementException { + KeyStore ks = KeyStore.getInstance(keystoreType); + SSLContext context = SSLContext.getInstance(protocol); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); + ks.load(null, null); + tmf.init(ks); + final Holder<X509Certificate[]> chain = new Holder<>(); + final X509TrustManager defaultTrustManager = (X509TrustManager) tmf + .getTrustManagers()[0]; + context.init(null, new TrustManager[] { new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] clientChain, + String authType) throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] serverChain, + String authType) throws CertificateException { + chain.value = serverChain; + defaultTrustManager.checkServerTrusted(serverChain, authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + throw new UnsupportedOperationException(); + } + } }, null); + SSLSocketFactory factory = context.getSocketFactory(); + try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) { + socket.setSoTimeout(timeout); + socket.startHandshake(); + } catch (SSLException e) { + // Ignore + } + return chain.value; + } + + private Map<URI, List<X509Certificate>> cache = new HashMap<>(); + + /** + * Gets the certificate chain for a service identified by URI. + * + * @param uri + * The URI of the (secure) service to identify. + * @return The certificate chain. Will be <tt>null</tt> if the service is + * not secure. + * @throws IOException + * If the service is unreachable or other connection problems + * occur. + * @throws GeneralSecurityException + * If any of a number of security-related problems occur, such + * as an inability to match detailed security protocols. + */ + public List<X509Certificate> getTrustsForURI(URI uri) throws IOException, + GeneralSecurityException { + if (!secure) + return null; + synchronized (this) { + if (!cache.containsKey(uri)) { + int port = uri.getPort(); + if (port == -1) + switch (uri.getScheme()) { + case "http": + port = 80; + break; + case "https": + port = 443; + break; + default: + return null; + } + X509Certificate[] chain = getCertificateChainForService( + uri.getHost(), port); + if (chain != null) + cache.put(uri, unmodifiableList(asList(chain))); + else + cache.put(uri, null); + } + return cache.get(uri); + } + } + + /** + * Flushes the cache. + */ + public void flushCache() { + synchronized (this) { + cache.clear(); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/Contextualizer.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/Contextualizer.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/Contextualizer.java new file mode 100644 index 0000000..884ff94 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/Contextualizer.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import javax.servlet.ServletContext; +import javax.ws.rs.core.UriInfo; + +import org.springframework.web.context.ServletContextAware; +import org.taverna.server.master.common.version.Version; + +/** + * Convert a string (URL, etc) to a version that is contextualized to the + * web-application. + * + * @author Donal Fellows + */ +public class Contextualizer implements ServletContextAware { + static final String ROOT_PLACEHOLDER = "%{WEBAPPROOT}"; + static final String VERSION_PLACEHOLDER = "%{VERSION}"; + static final String BASE_PLACEHOLDER = "%{BASEURL}"; + + /** + * Apply the contextualization operation. This consists of replacing the + * string <tt>{@value #ROOT_PLACEHOLDER}</tt> with the real root of the webapp. + * + * @param input + * the string to contextualize + * @return the contextualized string + */ + public String contextualize(String input) { + // Hack to work around bizarre CXF bug + String path = context.getRealPath("/").replace("%2D", "-"); + return input.replace(ROOT_PLACEHOLDER, path).replace( + VERSION_PLACEHOLDER, Version.JAVA); + } + + /** + * Apply the contextualization operation. This consists of replacing the + * string <tt>{@value #ROOT_PLACEHOLDER}</tt> with the real root of the + * webapp. + * + * @param ui + * Where to get information about the URL used to access the + * webapp. + * @param input + * the string to contextualize + * @return the contextualized string + */ + public String contextualize(UriInfo ui, String input) { + // Hack to work around bizarre CXF bug + String baseuri = ui.getBaseUri().toString().replace("%2D", "-"); + if (baseuri.endsWith("/")) + baseuri = baseuri.substring(0, baseuri.length() - 1); + return contextualize(input).replace(BASE_PLACEHOLDER, baseuri); + } + + private ServletContext context; + + @Override + public void setServletContext(ServletContext servletContext) { + context = servletContext; + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/DerbyUtils.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/DerbyUtils.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/DerbyUtils.java new file mode 100644 index 0000000..f8b39e3 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/DerbyUtils.java @@ -0,0 +1,68 @@ +package org.taverna.server.master.utils; + +import java.io.EOFException; +import java.io.IOException; +import java.io.Writer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Utility class, used to make Derby less broken. + * + * @see <a + * href="http://stackoverflow.com/questions/1004327/getting-rid-of-derby-log"> + * Getting rid of derby.log </a> + * @see <a + * href="http://stackoverflow.com/questions/3339736/set-system-property-with-spring-configuration-file"> + * Set system property with Spring configuration file </a> + */ +public class DerbyUtils { + /** + * A writer that channels things on to the log. + */ + public static final Writer TO_LOG = new DBLog(); + // Hack + public static final Writer DEV_NULL = TO_LOG; +} + +class DBLog extends Writer { + private Log log = LogFactory.getLog("Taverna.Server.Database"); + private StringBuilder sb = new StringBuilder(); + private boolean closed = false; + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + if (closed) + throw new EOFException(); + if (!log.isInfoEnabled()) + return; + sb.append(cbuf, off, len); + while (!closed) { + int idx = sb.indexOf("\n"), realIdx = idx; + if (idx < 0) + break; + char ch; + while (idx > 0 && ((ch = sb.charAt(idx - 1)) == '\r' || ch == ' ' || ch == '\t')) + idx--; + if (idx > 0) + log.info(sb.substring(0, idx)); + sb.delete(0, realIdx + 1); + } + } + + @Override + public void flush() throws IOException { + if (sb.length() > 0) { + log.info(sb.toString()); + sb = new StringBuilder(); + } + } + + @Override + public void close() throws IOException { + flush(); + closed = true; + sb = null; + } +} \ 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/utils/FilenameUtils.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/FilenameUtils.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/FilenameUtils.java new file mode 100644 index 0000000..19299e2 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/FilenameUtils.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import java.util.List; + +import javax.ws.rs.core.PathSegment; + +import org.taverna.server.master.common.DirEntryReference; +import org.taverna.server.master.exceptions.FilesystemAccessException; +import org.taverna.server.master.exceptions.NoDirectoryEntryException; +import org.taverna.server.master.interfaces.Directory; +import org.taverna.server.master.interfaces.DirectoryEntry; +import org.taverna.server.master.interfaces.File; +import org.taverna.server.master.interfaces.TavernaRun; + +/** + * Utility functions for getting entries from directories. + * + * @author Donal Fellows + */ +public class FilenameUtils { + private static final String TYPE_ERROR = "trying to take subdirectory of file"; + private static final String NO_FILE = "no such directory entry"; + private static final String NOT_A_FILE = "not a file"; + private static final String NOT_A_DIR = "not a directory"; + + /** + * Get a named directory entry from a workflow run. + * + * @param run + * The run whose working directory is to be used as the root of + * the search. + * @param name + * The name of the directory entry to look up. + * @return The directory entry whose name is equal to the last part of the + * path; an empty path will retrieve the working directory handle + * itself. + * @throws NoDirectoryEntryException + * If there is no such entry. + * @throws FilesystemAccessException + * If the directory isn't specified or isn't readable. + */ + public DirectoryEntry getDirEntry(TavernaRun run, String name) + throws FilesystemAccessException, NoDirectoryEntryException { + Directory dir = run.getWorkingDirectory(); + if (name == null || name.isEmpty()) + return dir; + DirectoryEntry found = dir; + boolean mustBeLast = false; + + // Must be nested loops; avoids problems with %-encoded "/" chars + for (String bit : name.split("/")) { + if (mustBeLast) + throw new FilesystemAccessException(TYPE_ERROR); + found = getEntryFromDir(bit, dir); + dir = null; + if (found instanceof Directory) { + dir = (Directory) found; + mustBeLast = false; + } else + mustBeLast = true; + } + return found; + } + + /** + * Get a named directory entry from a workflow run. + * + * @param run + * The run whose working directory is to be used as the root of + * the search. + * @param d + * The path segments describing what to look up. + * @return The directory entry whose name is equal to the last part of the + * path; an empty path will retrieve the working directory handle + * itself. + * @throws NoDirectoryEntryException + * If there is no such entry. + * @throws FilesystemAccessException + * If the directory isn't specified or isn't readable. + */ + public DirectoryEntry getDirEntry(TavernaRun run, List<PathSegment> d) + throws FilesystemAccessException, NoDirectoryEntryException { + Directory dir = run.getWorkingDirectory(); + if (d == null || d.isEmpty()) + return dir; + DirectoryEntry found = dir; + boolean mustBeLast = false; + + // Must be nested loops; avoids problems with %-encoded "/" chars + for (PathSegment segment : d) + for (String bit : segment.getPath().split("/")) { + if (mustBeLast) + throw new FilesystemAccessException(TYPE_ERROR); + found = getEntryFromDir(bit, dir); + dir = null; + if (found instanceof Directory) { + dir = (Directory) found; + mustBeLast = false; + } else + mustBeLast = true; + } + return found; + } + + /** + * Get a named directory entry from a workflow run. + * + * @param run + * The run whose working directory is to be used as the root of + * the search. + * @param d + * The directory reference describing what to look up. + * @return The directory entry whose name is equal to the last part of the + * path in the directory reference; an empty path will retrieve the + * working directory handle itself. + * @throws FilesystemAccessException + * If the directory isn't specified or isn't readable. + * @throws NoDirectoryEntryException + * If there is no such entry. + */ + public DirectoryEntry getDirEntry(TavernaRun run, DirEntryReference d) + throws FilesystemAccessException, NoDirectoryEntryException { + Directory dir = run.getWorkingDirectory(); + if (d == null || d.path == null || d.path.isEmpty()) + return dir; + DirectoryEntry found = dir; + boolean mustBeLast = false; + + for (String bit : d.path.split("/")) { + if (mustBeLast) + throw new FilesystemAccessException(TYPE_ERROR); + found = getEntryFromDir(bit, dir); + dir = null; + if (found instanceof Directory) { + dir = (Directory) found; + mustBeLast = false; + } else + mustBeLast = true; + } + return found; + } + + /** + * Get a named directory entry from a directory. + * + * @param name + * The name of the entry; must be "<tt>/</tt>"-free. + * @param dir + * The directory to look in. + * @return The directory entry whose name is equal to the given name. + * @throws NoDirectoryEntryException + * If there is no such entry. + * @throws FilesystemAccessException + * If the directory isn't specified or isn't readable. + */ + private DirectoryEntry getEntryFromDir(String name, Directory dir) + throws FilesystemAccessException, NoDirectoryEntryException { + if (dir == null) + throw new FilesystemAccessException(NO_FILE); + for (DirectoryEntry entry : dir.getContents()) + if (entry.getName().equals(name)) + return entry; + throw new NoDirectoryEntryException(NO_FILE); + } + + /** + * Get a named directory from a workflow run. + * + * @param run + * The run whose working directory is to be used as the root of + * the search. + * @param d + * The directory reference describing what to look up. + * @return The directory whose name is equal to the last part of the path in + * the directory reference; an empty path will retrieve the working + * directory handle itself. + * @throws FilesystemAccessException + * If the directory isn't specified or isn't readable, or if the + * name doesn't refer to a directory. + * @throws NoDirectoryEntryException + * If there is no such entry. + */ + public Directory getDirectory(TavernaRun run, DirEntryReference d) + throws FilesystemAccessException, NoDirectoryEntryException { + DirectoryEntry dirEntry = getDirEntry(run, d); + if (dirEntry instanceof Directory) + return (Directory) dirEntry; + throw new FilesystemAccessException(NOT_A_DIR); + } + + /** + * Get a named directory from a workflow run. + * + * @param run + * The run whose working directory is to be used as the root of + * the search. + * @param name + * The name of the directory to look up. + * @return The directory. + * @throws FilesystemAccessException + * If the directory isn't specified or isn't readable, or if the + * name doesn't refer to a directory. + * @throws NoDirectoryEntryException + * If there is no such entry. + */ + public Directory getDirectory(TavernaRun run, String name) + throws FilesystemAccessException, NoDirectoryEntryException { + DirectoryEntry dirEntry = getDirEntry(run, name); + if (dirEntry instanceof Directory) + return (Directory) dirEntry; + throw new FilesystemAccessException(NOT_A_DIR); + } + + /** + * Get a named file from a workflow run. + * + * @param run + * The run whose working directory is to be used as the root of + * the search. + * @param d + * The directory reference describing what to look up. + * @return The file whose name is equal to the last part of the path in the + * directory reference; an empty path will retrieve the working + * directory handle itself. + * @throws FilesystemAccessException + * If the file isn't specified or isn't readable, or if the name + * doesn't refer to a file. + * @throws NoDirectoryEntryException + * If there is no such entry. + */ + public File getFile(TavernaRun run, DirEntryReference d) + throws FilesystemAccessException, NoDirectoryEntryException { + DirectoryEntry dirEntry = getDirEntry(run, d); + if (dirEntry instanceof File) + return (File) dirEntry; + throw new FilesystemAccessException(NOT_A_FILE); + } + + /** + * Get a named file from a workflow run. + * + * @param run + * The run whose working directory is to be used as the root of + * the search. + * @param name + * The name of the file to look up. + * @return The file whose name is equal to the last part of the path in the + * directory reference; an empty path will retrieve the working + * directory handle itself. + * @throws FilesystemAccessException + * If the file isn't specified or isn't readable, or if the name + * doesn't refer to a file. + * @throws NoDirectoryEntryException + * If there is no such entry. + */ + public File getFile(TavernaRun run, String name) + throws FilesystemAccessException, NoDirectoryEntryException { + DirectoryEntry dirEntry = getDirEntry(run, name); + if (dirEntry instanceof File) + return (File) dirEntry; + throw new FilesystemAccessException(NOT_A_FILE); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/FlushThreadLocalCacheInterceptor.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/FlushThreadLocalCacheInterceptor.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/FlushThreadLocalCacheInterceptor.java new file mode 100644 index 0000000..ff52e81 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/FlushThreadLocalCacheInterceptor.java @@ -0,0 +1,18 @@ +package org.taverna.server.master.utils; + +import org.apache.cxf.jaxrs.provider.ProviderFactory; +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase; + +public class FlushThreadLocalCacheInterceptor extends + AbstractPhaseInterceptor<Message> { + public FlushThreadLocalCacheInterceptor() { + super(Phase.USER_LOGICAL_ENDING); + } + + @Override + public void handleMessage(Message message) { + ProviderFactory.getInstance(message).clearThreadLocalProxies(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/InvocationCounter.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/InvocationCounter.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/InvocationCounter.java new file mode 100644 index 0000000..55a260b --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/InvocationCounter.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +/** + * This class is responsible for counting all invocations of publicly-exposed + * methods of the webapp. It's connected to the webapp primarily through an + * AspectJ-style pointcut. + * + * @author Donal Fellows + */ +@Aspect +public class InvocationCounter { + private int count; + + @Before("@annotation(org.taverna.server.master.utils.InvocationCounter.CallCounted)") + public synchronized void count() { + count++; + } + + public synchronized int getCount() { + return count; + } + + /** + * Mark methods that should be counted by the invocation counter. + * + * @author Donal Fellows + */ + @Retention(RUNTIME) + @Documented + @Target(METHOD) + public static @interface CallCounted { + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/JCECheck.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/JCECheck.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/JCECheck.java new file mode 100644 index 0000000..252e316 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/JCECheck.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2012 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import static java.lang.Integer.MAX_VALUE; +import static javax.crypto.Cipher.getMaxAllowedKeyLength; +import static org.apache.commons.logging.LogFactory.getLog; + +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; + +import javax.annotation.PostConstruct; + +import org.apache.commons.logging.Log; + +/** + * Trivial bean that checks for whether the JCE policy files that allow + * unlimited strength security are present, and warns in the log if not. + * + * @author Donal Fellows + */ +public class JCECheck { + /** + * Write a message to the log that says whether an unlimited strength + * {@linkplain #Cipher cipher} is present. This is the official proxy for + * whether the unlimited strength JCE policy files have been installed; if + * absent, the message is logged as a warning, otherwise it is just + * informational. + */ + @PostConstruct + public void checkForUnlimitedJCE() { + Log log = getLog("Taverna.Server.Utils"); + + try { + if (getMaxAllowedKeyLength("AES") < MAX_VALUE) + log.warn("maximum key length very short; unlimited " + + "strength JCE policy files maybe missing"); + else + log.info("unlimited strength JCE policy in place"); + } catch (GeneralSecurityException e) { + log.warn("problem computing key length limits!", e); + } + } + + /** + * @return Whether the unlimited strength JCE policy files are present (or + * rather whether an unlimited strength {@linkplain #Cipher cipher} + * is permitted). + */ + public boolean isUnlimitedStrength() { + try { + return getMaxAllowedKeyLength("AES") == MAX_VALUE; + } catch (NoSuchAlgorithmException e) { + return false; + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/JDOSupport.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/JDOSupport.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/JDOSupport.java new file mode 100644 index 0000000..ba9ec81 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/JDOSupport.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.apache.commons.logging.LogFactory.getLog; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.WeakHashMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.PreDestroy; +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; +import javax.jdo.Query; +import javax.jdo.Transaction; + +import org.apache.commons.logging.Log; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Required; + +/** + * Simple support class that wraps up and provides access to the correct parts + * of JDO. + * + * @author Donal Fellows + * + * @param <T> The context class that the subclass will be working with. + */ +public abstract class JDOSupport<T> { + private Class<T> contextClass; + private PersistenceManagerBuilder pmb; + + /** + * Instantiate this class, supplying it a handle to the class that will be + * used to provide context for queries and accesses. + * + * @param contextClass + * Must match the type parameter to the class itself. + */ + protected JDOSupport(@Nonnull Class<T> contextClass) { + this.contextClass = contextClass; + } + + /** + * @param persistenceManagerBuilder + * The JDO engine to use for managing persistence. + */ + @Required + public void setPersistenceManagerBuilder( + PersistenceManagerBuilder persistenceManagerBuilder) { + pmb = persistenceManagerBuilder; + } + + private PersistenceManager pm() { + if (isPersistent()) + return pmb.getPersistenceManager(); + return null; + } + + /** + * Has this class actually been configured with a persistence manager by + * Spring? + * + * @return Whether there is a persistence manager installed. + */ + protected boolean isPersistent() { + return pmb != null; + } + + /** + * Get an instance of a query in JDOQL. + * + * @param filter + * The filter part of the query. + * @return The query, which should be executed to retrieve the results. + */ + @Nonnull + protected Query query(@Nonnull String filter) { + return pm().newQuery(contextClass, filter); + } + + /** + * Get an instance of a named query attached to the context class (as an + * annotation). + * + * @param name + * The name of the query. + * @return The query, which should be executed to retrieve the results. + * @see javax.jdo.annotations.Query + */ + @Nonnull + protected Query namedQuery(@Nonnull String name) { + return pm().newNamedQuery(contextClass, name); + } + + /** + * Make an instance of the context class persist in the database. It's + * identity must not already exist. + * + * @param value + * The instance to persist. + * @return The persistence-coupled instance. + */ + @Nullable + protected T persist(@Nullable T value) { + if (value == null) + return null; + return pm().makePersistent(value); + } + + /** + * Make a non-persistent (i.e., will hold its value past the end of the + * transaction) copy of a persistence-coupled instance of the context class. + * + * @param value + * The value to decouple. + * @return The non-persistent copy. + */ + @Nullable + protected T detach(@Nullable T value) { + if (value == null) + return null; + return pm().detachCopy(value); + } + + /** + * Look up an instance of the context class by its identity. + * + * @param id + * The identity of the object. + * @return The instance, which is persistence-coupled. + */ + @Nullable + protected T getById(Object id) { + try { + return pm().getObjectById(contextClass, id); + } catch (Exception e) { + return null; + } + } + + /** + * Delete a persistence-coupled instance of the context class. + * + * @param value + * The value to delete. + */ + protected void delete(@Nullable T value) { + if (value != null) + pm().deletePersistent(value); + } + + /** + * Manages integration of JDO transactions with Spring. + * + * @author Donal Fellows + */ + @Aspect + public static class TransactionAspect { + private Object lock = new Object(); + private Log log = getLog("Taverna.Server.Utils"); + private volatile int txid; + + @Around(value = "@annotation(org.taverna.server.master.utils.JDOSupport.WithinSingleTransaction) && target(support)", argNames = "support") + Object applyTransaction(ProceedingJoinPoint pjp, JDOSupport<?> support) + throws Throwable { + synchronized (lock) { + PersistenceManager pm = support.pm(); + int id = ++txid; + Transaction tx = (pm == null) ? null : pm.currentTransaction(); + if (tx != null && tx.isActive()) + tx = null; + if (tx != null) { + if (log.isDebugEnabled()) + log.debug("starting transaction #" + id); + tx.begin(); + } + try { + Object result = pjp.proceed(); + if (tx != null) { + tx.commit(); + if (log.isDebugEnabled()) + log.debug("committed transaction #" + id); + } + tx = null; + return result; + } catch (Throwable t) { + try { + if (tx != null) { + tx.rollback(); + if (log.isDebugEnabled()) + log.debug("rolled back transaction #" + id); + } + } catch (JDOException e) { + log.warn("rollback failed unexpectedly", e); + } + throw t; + } + } + } + } + + /** + * Mark a method (of a subclass of {@link JDOSupport}) as having a + * transaction wrapped around it. The transactions are managed correctly in + * the multi-threaded case. + * + * @author Donal Fellows + */ + @Target(METHOD) + @Retention(RUNTIME) + @Documented + public @interface WithinSingleTransaction { + } + + /** + * Manages {@linkplain PersistenceManager persistence managers} in a way + * that doesn't cause problems when the web application is unloaded. + * + * @author Donal Fellows + */ + public static class PersistenceManagerBuilder { + private PersistenceManagerFactory pmf; + private WeakHashMap<Thread, PersistenceManager> cache = new WeakHashMap<>(); + + /** + * @param persistenceManagerFactory + * The JDO engine to use for managing persistence. + */ + @Required + public void setPersistenceManagerFactory( + PersistenceManagerFactory persistenceManagerFactory) { + pmf = persistenceManagerFactory; + } + + @Nonnull + public PersistenceManager getPersistenceManager() { + if (cache == null) + return pmf.getPersistenceManager(); + Thread t = Thread.currentThread(); + PersistenceManager pm = cache.get(t); + if (pm == null && pmf != null) { + pm = pmf.getPersistenceManager(); + cache.put(t, pm); + } + return pm; + } + + @PreDestroy + void clearThreadCache() { + WeakHashMap<Thread, PersistenceManager> cache = this.cache; + this.cache = null; + for (PersistenceManager pm : cache.values()) + if (pm != null) + pm.close(); + cache.clear(); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/LoggingDerbyAdapter.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/LoggingDerbyAdapter.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/LoggingDerbyAdapter.java new file mode 100644 index 0000000..a8aa937 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/LoggingDerbyAdapter.java @@ -0,0 +1,138 @@ +package org.taverna.server.master.utils; + +import static java.lang.System.currentTimeMillis; +import static java.lang.Thread.sleep; +import static org.apache.commons.logging.LogFactory.getLog; + +import java.sql.DatabaseMetaData; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.datanucleus.store.rdbms.adapter.DerbyAdapter; +import org.datanucleus.store.rdbms.identifier.IdentifierFactory; +import org.datanucleus.store.rdbms.key.CandidateKey; +import org.datanucleus.store.rdbms.key.ForeignKey; +import org.datanucleus.store.rdbms.key.Index; +import org.datanucleus.store.rdbms.key.PrimaryKey; +import org.datanucleus.store.rdbms.sql.SQLTable; +import org.datanucleus.store.rdbms.table.Column; +import org.datanucleus.store.rdbms.table.Table; +import org.datanucleus.store.rdbms.table.TableImpl; +import org.datanucleus.store.rdbms.table.ViewImpl; + +/** + * Evil hack to allow logging of the DDL spat out to Derby. + * + * @author Donal Fellows + */ +public class LoggingDerbyAdapter extends DerbyAdapter { + Log log = getLog("Taverna.Server.SQL"); + + private StringBuilder ddl = new StringBuilder(); + private volatile long timeout; + private Thread timer; + + private synchronized void logDDL() { + if (ddl.length() > 0) { + log.info("Data definition language:\n" + ddl); + ddl.setLength(0); + } + timer = null; + } + + private synchronized void doLog(String item) { + ddl.append(item); + if (!item.endsWith("\n")) + ddl.append('\n'); + timeout = currentTimeMillis() + 5000; + if (timer == null) + timer = new OneShotThread("DDL logger timeout", new Runnable() { + @Override + public void run() { + try { + while (timeout > currentTimeMillis()) + sleep(1000); + } catch (InterruptedException e) { + // Ignore + } + logDDL(); + } + }); + } + + /** + * Creates an Apache Derby adapter based on the given metadata which logs + * the DDL it creates. + */ + public LoggingDerbyAdapter(DatabaseMetaData metadata) { + super(metadata); + } + + @Override + public String getCreateTableStatement(TableImpl table, Column[] columns, + Properties props, IdentifierFactory factory) { + String statement = super.getCreateTableStatement(table, columns, props, + factory); + doLog(statement); + return statement; + } + + @Override + public String getCreateIndexStatement(Index index, IdentifierFactory factory) { + String statement = super.getCreateIndexStatement(index, factory); + doLog(statement); + return statement; + } + + @Override + public String getAddCandidateKeyStatement(CandidateKey ck, + IdentifierFactory factory) { + String statement = super.getAddCandidateKeyStatement(ck, factory); + doLog(statement); + return statement; + } + + @Override + public String getAddPrimaryKeyStatement(PrimaryKey pk, + IdentifierFactory factory) { + String statement = super.getAddPrimaryKeyStatement(pk, factory); + doLog(statement); + return statement; + } + + @Override + public String getAddColumnStatement(Table table, Column col) { + String statement = super.getAddColumnStatement(table, col); + doLog(statement); + return statement; + } + + @Override + public String getAddForeignKeyStatement(ForeignKey fk, + IdentifierFactory factory) { + String statement = super.getAddForeignKeyStatement(fk, factory); + doLog(statement); + return statement; + } + + @Override + public String getDeleteTableStatement(SQLTable tbl) { + String statement = super.getDeleteTableStatement(tbl); + doLog(statement); + return statement; + } + + @Override + public String getDropTableStatement(Table table) { + String statement = super.getDropTableStatement(table); + doLog(statement); + return statement; + } + + @Override + public String getDropViewStatement(ViewImpl view) { + String statement = super.getDropViewStatement(view); + doLog(statement); + return statement; + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/OneShotThread.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/OneShotThread.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/OneShotThread.java new file mode 100644 index 0000000..68b813d --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/OneShotThread.java @@ -0,0 +1,10 @@ +package org.taverna.server.master.utils; + +public class OneShotThread extends Thread { + public OneShotThread(String name, Runnable target) { + super(target, name); + setContextClassLoader(null); + setDaemon(true); + start(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/RestUtils.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/RestUtils.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/RestUtils.java new file mode 100644 index 0000000..a892b52 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/RestUtils.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import javax.ws.rs.OPTIONS; +import javax.ws.rs.core.Response; + +/** + * Utilities that make it easier to write REST services. + * + * @author Donal Fellows + */ +public class RestUtils { + /** + * Generate a response to an HTTP OPTIONS request. + * + * @param methods + * The state-changing methods supported, if any. + * @return the required response + * @see OPTIONS + */ + public static Response opt(String... methods) { + StringBuilder sb = new StringBuilder("GET,"); + for (String m : methods) + sb.append(m).append(","); + sb.append("HEAD,OPTIONS"); + return Response.ok().header("Allow", sb.toString()).entity("").build(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/RuntimeExceptionWrapper.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/RuntimeExceptionWrapper.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/RuntimeExceptionWrapper.java new file mode 100644 index 0000000..0b2d4ea --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/RuntimeExceptionWrapper.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.taverna.server.master.exceptions.GeneralFailureException; + +/** + * Aspect used to convert {@linkplain RuntimeException runtime exceptions} into + * a form that can be nicely conveyed to the outside world as HTTP errors. + * + * @author Donal Fellows + */ +@Aspect +public class RuntimeExceptionWrapper { + /** + * Map an unexpected exception to one that can be correctly reported as a + * problem. + * + * @param exn + * The runtime exception being trapped. + * @throws GeneralFailureException + * The known exception type that it is mapped to. + */ + @AfterThrowing(pointcut = "execution(* org.taverna.server.master.rest..*(..)) && !bean(*Provider.*)", throwing = "exn") + public void wrapRuntimeException(RuntimeException exn) + throws GeneralFailureException { + // Exclude security-related exceptions + if (exn.getClass().getName().startsWith("org.springframework.security.")) + return; + throw new GeneralFailureException(exn); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/UsernamePrincipal.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/UsernamePrincipal.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/UsernamePrincipal.java new file mode 100644 index 0000000..9bbcc2f --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/UsernamePrincipal.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import java.io.Serializable; +import java.security.Principal; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * A simple serializable principal that just records the name. + * + * @author Donal Fellows + */ +public class UsernamePrincipal implements Principal, Serializable { + private static final long serialVersionUID = 2703493248562435L; + public UsernamePrincipal(String username) { + this.name = username; + } + + public UsernamePrincipal(Principal other) { + this.name = other.getName(); + } + + public UsernamePrincipal(Authentication auth) { + this(auth.getPrincipal()); + } + + public UsernamePrincipal(Object principal) { + if (principal instanceof Principal) + this.name = ((Principal) principal).getName(); + else if (principal instanceof String) + this.name = (String) principal; + else if (principal instanceof UserDetails) + this.name = ((UserDetails) principal).getUsername(); + else + this.name = principal.toString(); + } + + private String name; + + @Override + public String getName() { + return name; + } + + @Override + public String toString() { + return "Principal<" + name + ">"; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Principal) { + Principal p = (Principal) o; + return name.equals(p.getName()); + } + return false; + } + + @Override + public int hashCode() { + return name.hashCode(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/WSDLHeadOptionsInterceptor.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/WSDLHeadOptionsInterceptor.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/WSDLHeadOptionsInterceptor.java new file mode 100644 index 0000000..96cdc6d --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/WSDLHeadOptionsInterceptor.java @@ -0,0 +1,65 @@ +package org.taverna.server.master.utils; + +import static org.apache.commons.logging.LogFactory.getLog; +import static org.apache.cxf.common.util.UrlUtils.parseQueryString; +import static org.apache.cxf.message.Message.HTTP_REQUEST_METHOD; +import static org.apache.cxf.message.Message.QUERY_STRING; +import static org.apache.cxf.message.Message.REQUEST_URL; +import static org.apache.cxf.phase.Phase.READ; + +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.cxf.binding.soap.interceptor.EndpointSelectionInterceptor; +import org.apache.cxf.interceptor.Fault; +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; + + +/** + * Thunk for TAVSERV-293. + * + * @author Donal Fellows (based on work by Daniel Hagen) + */ +public class WSDLHeadOptionsInterceptor extends + AbstractPhaseInterceptor<Message> { + public static final Log log = getLog("Taverna.Server.Utils"); + + public WSDLHeadOptionsInterceptor() { + super(READ); + getAfter().add(EndpointSelectionInterceptor.class.getName()); + } + + @Override + public void handleMessage(Message message) throws Fault { + String method = (String) message.get(HTTP_REQUEST_METHOD); + String query = (String) message.get(QUERY_STRING); + + if (("HEAD".equals(method) || "OPTIONS".equals(method)) + && query != null && !query.trim().isEmpty() + && isRecognizedQuery(query)) { + log.debug("adjusting message request method " + method + " for " + + message.get(REQUEST_URL) + " to GET"); + message.put(HTTP_REQUEST_METHOD, "GET"); + } + } + + /* + * Stolen from http://permalink.gmane.org/gmane.comp.apache.cxf.user/20037 + * which is itself in turn stolen from + * org.apache.cxf.frontend.WSDLGetInterceptor.isRecognizedQuery + */ + /** + * Is this a query for WSDL or XSD relating to it? + * + * @param query + * The query string to check + * @return If the query is one to handle. + * @see org.apache.cxf.frontend.WSDLGetInterceptor#isRecognizedQuery(Map,String,String,org.apache.cxf.service.model.EndpointInfo) + * WSDLGetInterceptor + */ + private boolean isRecognizedQuery(String query) { + Map<String, String> map = parseQueryString(query); + return map.containsKey("wsdl") || map.containsKey("xsd"); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/WebappAwareDataSource.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/WebappAwareDataSource.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/WebappAwareDataSource.java new file mode 100644 index 0000000..03fc749 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/WebappAwareDataSource.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import static java.lang.Thread.currentThread; +import static java.sql.DriverManager.deregisterDriver; +import static java.sql.DriverManager.getDrivers; +import static org.taverna.server.master.utils.Contextualizer.ROOT_PLACEHOLDER; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Enumeration; + +import javax.annotation.PreDestroy; + +import org.apache.commons.dbcp.BasicDataSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Required; + +/** + * Add some awareness of the context so that we can locate databases internally + * to the webapp. + * + * @author Donal Fellows + */ +public class WebappAwareDataSource extends BasicDataSource { + Log log = LogFactory.getLog("Taverna.Server.Utils"); + private transient boolean init; + private Contextualizer ctxt; + private String shutdownUrl; + + @Required + public void setContextualizer(Contextualizer ctxt) { + this.ctxt = ctxt; + } + + /** + * A JDBC connection URL to use on shutting down the database. If not set, + * do nothing special. + * + * @param url + */ + public void setShutdownUrl(String url) { + shutdownUrl = url; + } + + private void doInit() { + synchronized (this) { + if (!init) { + setDriverClassLoader(currentThread().getContextClassLoader()); + String url = getUrl(); + if (url.contains(ROOT_PLACEHOLDER)) { + String newurl = ctxt.contextualize(url); + setUrl(newurl); + log.info("mapped " + url + " to " + newurl); + } else { + log.info("did not find " + ROOT_PLACEHOLDER + " in " + url); + } + init = true; + } + } + } + + // -=-=-=-=-=-=-=-=-=-=- HOOKS -=-=-=-=-=-=-=-=-=-=- + + @Override + public Connection getConnection() throws SQLException { + doInit(); + return super.getConnection(); + } + + @Override + public void setLogWriter(PrintWriter pw) throws SQLException { + doInit(); + super.setLogWriter(pw); + } + + @Override + public void setLoginTimeout(int num) throws SQLException { + doInit(); + super.setLoginTimeout(num); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + doInit(); + return super.getLogWriter(); + } + + @Override + public int getLoginTimeout() throws SQLException { + doInit(); + return super.getLoginTimeout(); + } + + @PreDestroy + void realClose() { + try { + close(); + } catch (SQLException e) { + log.warn("problem shutting down DB connection", e); + } + try { + if (shutdownUrl != null) + DriverManager.getConnection(ctxt.contextualize(shutdownUrl)); + } catch (SQLException e) { + // Expected; ignore it + } + log = null; + dropDriver(); + } + + private void dropDriver() { + Enumeration<Driver> drivers = getDrivers(); + while (drivers.hasMoreElements()) { + Driver d = drivers.nextElement(); + if (d.getClass().getClassLoader() == getDriverClassLoader() + && d.getClass().getName().equals(getDriverClassName())) { + try { + deregisterDriver(d); + } catch (SQLException e) { + } + break; + } + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/X500Utils.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/X500Utils.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/X500Utils.java new file mode 100644 index 0000000..da4cff0 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/X500Utils.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.utils; + +import static javax.security.auth.x500.X500Principal.RFC2253; + +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.PreDestroy; +import javax.security.auth.x500.X500Principal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Support class that factors out some of the messier parts of working with + * X.500 identities and X.509 certificates. + * + * @author Donal Fellows + */ +public class X500Utils { + private Log log = LogFactory.getLog("Taverna.Server.Utils"); + + @PreDestroy + void closeLog() { + log = null; + } + + private static final char DN_SEPARATOR = ','; + private static final char DN_ESCAPE = '\\'; + private static final char DN_QUOTE = '"'; + + /** + * Parse the DN from the Principal and extract the CN field. + * + * @param id + * The identity to extract the distinguished name from. + * @param fields + * The names to look at when finding the field to return. Each + * should be an upper-cased string. + * @return The common-name part of the distinguished name, or the literal + * string "<tt>none</tt>" if there is no CN. + */ + public String getName(X500Principal id, String... fields) { + String dn = id.getName(RFC2253); + + int i = 0; + int startIndex = 0; + boolean ignoreThisChar = false; + boolean inQuotes = false; + Map<String, String> tokenized = new HashMap<>(); + + for (i = 0; i < dn.length(); i++) + if (ignoreThisChar) + ignoreThisChar = false; + else if (dn.charAt(i) == DN_QUOTE) + inQuotes = !inQuotes; + else if (inQuotes) + continue; + else if (dn.charAt(i) == DN_ESCAPE) + ignoreThisChar = true; + else if ((dn.charAt(i) == DN_SEPARATOR) && !ignoreThisChar) { + storeDNField(tokenized, dn.substring(startIndex, i).trim() + .split("=", 2)); + startIndex = i + 1; + } + if (inQuotes || ignoreThisChar) + log.warn("was parsing invalid DN format"); + // Add last token - after the last delimiter + storeDNField(tokenized, dn.substring(startIndex).trim().split("=", 2)); + + for (String field : fields) { + String value = tokenized.get(field); + if (value != null) + return value; + } + return "none"; + } + + private void storeDNField(Map<String, String> container, String[] split) { + if (split == null || split.length != 2) + return; + String key = split[0].toUpperCase(); + if (container.containsKey(key)) + log.warn("duplicate field in DN: " + key); + // LATER: Should the field be de-quoted? + container.put(key, split[1]); + } + + /** + * Get the serial number from a certificate as a hex string. + * + * @param cert + * The certificate to extract from. + * @return A hex string, in upper-case. + */ + public String getSerial(X509Certificate cert) { + return new BigInteger(1, cert.getSerialNumber().toByteArray()) + .toString(16).toUpperCase(); + } +} \ 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/utils/package-info.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/package-info.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/package-info.java new file mode 100644 index 0000000..612e61c --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/utils/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +/** + * Miscellaneous utility classes. Includes aspects that might be attached + * for purposes such as transaction management and invocation tracking. + */ +package org.taverna.server.master.utils; http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/CompletionNotifier.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/CompletionNotifier.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/CompletionNotifier.java new file mode 100644 index 0000000..10b3830 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/CompletionNotifier.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.worker; + + +/** + * How to convert a notification about the completion of a job into a message. + * + * @author Donal Fellows + */ +public interface CompletionNotifier { + /** + * @return The name of this notifier. + */ + String getName(); + + /** + * Called to get the content of a message that a workflow run has finished. + * + * @param name + * The name of the run. + * @param run + * What run are we talking about. + * @param code + * What the exit code was. + * @return The plain-text content of the message. + */ + String makeCompletionMessage(String name, RemoteRunDelegate run, int code); + + /** + * Called to get the subject of the message to dispatch. + * + * @param name + * The name of the run. + * @param run + * What run are we talking about. + * @param code + * What the exit code was. + * @return The plain-text subject of the message. + */ + String makeMessageSubject(String name, RemoteRunDelegate run, int code); +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/FactoryBean.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/FactoryBean.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/FactoryBean.java new file mode 100644 index 0000000..5d0f371 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/FactoryBean.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2013 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.worker; + +import org.taverna.server.master.notification.atom.EventDAO; + +/** + * What the remote run really needs of its factory. + * + * @author Donal Fellows + */ +public interface FactoryBean { + /** + * @return Whether a run can actually be started at this time. + */ + boolean isAllowingRunsToStart(); + + /** + * @return a handle to the master Atom event feed (<i>not</i> the per-run + * feed) + */ + EventDAO getMasterEventFeed(); +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PasswordIssuer.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PasswordIssuer.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PasswordIssuer.java new file mode 100644 index 0000000..d3c5b8a --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PasswordIssuer.java @@ -0,0 +1,57 @@ +package org.taverna.server.master.worker; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A simple password issuing bean. + * + * @author Donal Fellows + */ +public class PasswordIssuer { + private static final char[] ALPHABET = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', + '8', '9', '0', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', + ',', '.', '<', '>', '/', '?', ':', ';', '-', '_', '+', '[', ']', + '{', '}', '`', '~' }; + private Log log = LogFactory.getLog("Taverna.Server.Worker"); + private SecureRandom r; + private int length; + + public PasswordIssuer() { + r = new SecureRandom(); + log.info("constructing passwords with " + r.getAlgorithm()); + setLength(8); + } + + public PasswordIssuer(String algorithm) throws NoSuchAlgorithmException { + r = SecureRandom.getInstance(algorithm); + log.info("constructing passwords with " + r.getAlgorithm()); + setLength(8); + } + + public void setLength(int length) { + this.length = length; + log.info("issued password will be " + this.length + + " symbols chosen from " + ALPHABET.length); + } + + /** + * Issue a password. + * + * @return The new password. + */ + public String issue() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) + sb.append(ALPHABET[r.nextInt(ALPHABET.length)]); + log.info("issued new password of length " + sb.length()); + return sb.toString(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PolicyImpl.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PolicyImpl.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PolicyImpl.java new file mode 100644 index 0000000..f5613c7 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PolicyImpl.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.worker; + +import static org.taverna.server.master.identity.WorkflowInternalAuthProvider.PREFIX; + +import java.net.URI; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Required; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.taverna.server.master.common.Roles; +import org.taverna.server.master.common.Workflow; +import org.taverna.server.master.exceptions.NoCreateException; +import org.taverna.server.master.exceptions.NoDestroyException; +import org.taverna.server.master.exceptions.NoUpdateException; +import org.taverna.server.master.interfaces.Policy; +import org.taverna.server.master.interfaces.TavernaRun; +import org.taverna.server.master.interfaces.TavernaSecurityContext; +import org.taverna.server.master.utils.UsernamePrincipal; + +/** + * Basic policy implementation that allows any workflow to be instantiated by + * any user, but which does not permit users to access each others workflow + * runs. It also imposes a global limit on the number of workflow runs at once. + * + * @author Donal Fellows + */ +class PolicyImpl implements Policy { + Log log = LogFactory.getLog("Taverna.Server.Worker.Policy"); + private PolicyLimits limits; + private RunDBSupport runDB; + + @Required + public void setLimits(PolicyLimits limits) { + this.limits = limits; + } + + @Required + public void setRunDB(RunDBSupport runDB) { + this.runDB = runDB; + } + + @Override + public int getMaxRuns() { + return limits.getMaxRuns(); + } + + @Override + public Integer getMaxRuns(UsernamePrincipal user) { + return null; + } + + @Override + public int getOperatingLimit() { + return limits.getOperatingLimit(); + } + + @Override + public List<URI> listPermittedWorkflowURIs(UsernamePrincipal user) { + return limits.getPermittedWorkflowURIs(); + } + + private boolean isSelfAccess(String runId) { + Authentication auth = SecurityContextHolder.getContext() + .getAuthentication(); + boolean self = false; + String id = null; + for (GrantedAuthority a : auth.getAuthorities()) { + String aa = a.getAuthority(); + if (aa.equals(Roles.SELF)) { + self = true; + continue; + } + if (!aa.startsWith(PREFIX)) + continue; + id = aa.substring(PREFIX.length()); + } + return self && runId.equals(id); + } + + @Override + public boolean permitAccess(UsernamePrincipal user, TavernaRun run) { + String username = user.getName(); + TavernaSecurityContext context = run.getSecurityContext(); + if (context.getOwner().getName().equals(username)) { + if (log.isDebugEnabled()) + log.debug("granted access by " + user.getName() + " to " + + run.getId()); + return true; + } + if (isSelfAccess(run.getId())) { + if (log.isDebugEnabled()) + log.debug("access by workflow to itself: " + run.getId()); + return true; + } + if (log.isDebugEnabled()) + log.debug("considering access by " + user.getName() + " to " + + run.getId()); + return context.getPermittedReaders().contains(username); + } + + @Override + public void permitCreate(UsernamePrincipal user, Workflow workflow) + throws NoCreateException { + if (user == null) + throw new NoCreateException( + "anonymous workflow creation not allowed"); + if (runDB.countRuns() >= getMaxRuns()) + throw new NoCreateException("server load exceeded; please wait"); + } + + @Override + public synchronized void permitDestroy(UsernamePrincipal user, TavernaRun run) + throws NoDestroyException { + if (user == null) + throw new NoDestroyException(); + String username = user.getName(); + TavernaSecurityContext context = run.getSecurityContext(); + if (context.getOwner() == null + || context.getOwner().getName().equals(username)) + return; + if (!context.getPermittedDestroyers().contains(username)) + throw new NoDestroyException(); + } + + @Override + public void permitUpdate(UsernamePrincipal user, TavernaRun run) + throws NoUpdateException { + if (user == null) + throw new NoUpdateException( + "workflow run not owned by you and you're not granted access"); + TavernaSecurityContext context = run.getSecurityContext(); + if (context.getOwner().getName().equals(user.getName())) + return; + if (isSelfAccess(run.getId())) { + if (log.isDebugEnabled()) + log.debug("update access by workflow to itself: " + run.getId()); + return; + } + if (!context.getPermittedUpdaters().contains(user.getName())) + throw new NoUpdateException( + "workflow run not owned by you and you're not granted access"); + } + + @Override + public void setPermittedWorkflowURIs(UsernamePrincipal user, + List<URI> permitted) { + limits.setPermittedWorkflowURIs(permitted); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PolicyLimits.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PolicyLimits.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PolicyLimits.java new file mode 100644 index 0000000..8cbc7ea --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/worker/PolicyLimits.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.worker; + +import java.net.URI; +import java.util.List; + +import org.taverna.server.master.common.Status; + +/** + * The worker policy delegates certain limits to the state model of the + * particular worker. + * + * @author Donal Fellows + */ +public interface PolicyLimits { + /** + * @return the maximum number of extant workflow runs in any state + */ + int getMaxRuns(); + + /** + * @return the maximum number of workflow runs in the + * {@linkplain Status#Operating operating} state. + */ + int getOperatingLimit(); + + /** + * @return the list of URIs to workflows that may be used to create workflow + * runs. If empty or <tt>null</tt>, no restriction is present. + */ + List<URI> getPermittedWorkflowURIs(); + + /** + * @param permitted + * the list of URIs to workflows that may be used to create + * workflow runs. + */ + void setPermittedWorkflowURIs(List<URI> permitted); +}
