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

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

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Policy.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Policy.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Policy.java
new file mode 100644
index 0000000..b09e0bd
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Policy.java
@@ -0,0 +1,133 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.net.URI;
+import java.util.List;
+
+import org.taverna.server.master.common.Status;
+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.utils.UsernamePrincipal;
+
+/**
+ * Simple policy interface.
+ * 
+ * @author Donal Fellows
+ */
+public interface Policy {
+	/**
+	 * @return The maximum number of runs that the system can support.
+	 */
+	int getMaxRuns();
+
+	/**
+	 * Get the limit on the number of runs for this user.
+	 * 
+	 * @param user
+	 *            Who to get the limit for
+	 * @return The maximum number of runs for this user, or <tt>null</tt> if no
+	 *         per-user limit is imposed and only system-wide limits are to be
+	 *         enforced.
+	 */
+	Integer getMaxRuns(UsernamePrincipal user);
+
+	/**
+	 * Test whether the user can create an instance of the given workflow.
+	 * 
+	 * @param user
+	 *            Who wants to do the creation.
+	 * @param workflow
+	 *            The workflow they wish to instantiate.
+	 * @throws NoCreateException
+	 *             If they may not instantiate it.
+	 */
+	void permitCreate(UsernamePrincipal user, Workflow workflow)
+			throws NoCreateException;
+
+	/**
+	 * Test whether the user can destroy a workflow instance run or manipulate
+	 * its expiry date.
+	 * 
+	 * @param user
+	 *            Who wants to do the deletion.
+	 * @param run
+	 *            What they want to delete.
+	 * @throws NoDestroyException
+	 *             If they may not destroy it.
+	 */
+	void permitDestroy(UsernamePrincipal user, TavernaRun run)
+			throws NoDestroyException;
+
+	/**
+	 * Return whether the user has access to a particular workflow run.
+	 * <b>Note</b> that this does not throw any exceptions!
+	 * 
+	 * @param user
+	 *            Who wants to read the workflow's state.
+	 * @param run
+	 *            What do they want to read from.
+	 * @return Whether they can read it. Note that this check is always applied
+	 *         before testing whether the workflow can be updated or deleted by
+	 *         the user.
+	 */
+	boolean permitAccess(UsernamePrincipal user, TavernaRun run);
+
+	/**
+	 * Test whether the user can modify a workflow run (other than for its
+	 * expiry date).
+	 * 
+	 * @param user
+	 *            Who wants to do the modification.
+	 * @param run
+	 *            What they want to modify.
+	 * @throws NoUpdateException
+	 *             If they may not modify it.
+	 */
+	void permitUpdate(UsernamePrincipal user, TavernaRun run)
+			throws NoUpdateException;
+
+	/**
+	 * Get the URIs of the workflows that the given user may execute.
+	 * 
+	 * @param user
+	 *            Who are we finding out on behalf of.
+	 * @return A list of workflow URIs that they may instantiate, or
+	 *         <tt>null</tt> if any workflow may be submitted.
+	 */
+	List<URI> listPermittedWorkflowURIs(UsernamePrincipal user);
+
+	/**
+	 * @return The maximum number of {@linkplain Status#Operating operating}
+	 *         runs that the system can support.
+	 */
+	int getOperatingLimit();
+
+	/**
+	 * Set the URIs of the workflows that the given user may execute.
+	 * 
+	 * @param user
+	 *            Who are we finding out on behalf of.
+	 * @param permitted
+	 *            A list of workflow URIs that they may instantiate.
+	 */
+	void setPermittedWorkflowURIs(UsernamePrincipal user, List<URI> permitted);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/RunStore.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/RunStore.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/RunStore.java
new file mode 100644
index 0000000..b0d817a
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/RunStore.java
@@ -0,0 +1,95 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.Map;
+
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Interface to the mechanism that looks after the mapping of names to runs.
+ * Instances of this class may also be responsible for enforcing timely cleanup
+ * of expired workflows.
+ * 
+ * @author Donal Fellows.
+ */
+public interface RunStore {
+	/**
+	 * Obtain the workflow run for a given user and name.
+	 * 
+	 * @param user
+	 *            Who wants to do the lookup.
+	 * @param p
+	 *            The general policy system context.
+	 * @param uuid
+	 *            The handle for the run.
+	 * @return The workflow instance run.
+	 * @throws UnknownRunException
+	 *             If the lookup fails (either because it does not exist or
+	 *             because it is not permitted for the user by the policy).
+	 */
+	TavernaRun getRun(UsernamePrincipal user, Policy p, String uuid)
+			throws UnknownRunException;
+
+	/**
+	 * Obtain the named workflow run.
+	 * 
+	 * @param uuid
+	 *            The handle for the run.
+	 * @return The workflow instance run.
+	 * @throws UnknownRunException
+	 *             If the lookup fails (either because it does not exist or
+	 *             because it is not permitted for the user by the policy).
+	 */
+	public TavernaRun getRun(String uuid) throws UnknownRunException;
+
+	/**
+	 * List the runs that a particular user may access.
+	 * 
+	 * @param user
+	 *            Who wants to do the lookup, or <code>null</code> if it is
+	 *            being done "by the system" when the full mapping should be
+	 *            returned.
+	 * @param p
+	 *            The general policy system context.
+	 * @return A mapping from run names to run instances.
+	 */
+	Map<String, TavernaRun> listRuns(UsernamePrincipal user, Policy p);
+
+	/**
+	 * Adds a workflow instance run to the store. Note that this operation is
+	 * <i>not</i> expected to be security-checked; that is the callers'
+	 * responsibility.
+	 * 
+	 * @param run
+	 *            The run itself.
+	 * @return The name of the run.
+	 */
+	String registerRun(TavernaRun run);
+
+	/**
+	 * Removes a run from the store. Note that this operation is <i>not</i>
+	 * expected to be security-checked; that is the callers' responsibility.
+	 * 
+	 * @param uuid
+	 *            The name of the run.
+	 */
+	void unregisterRun(String uuid);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/SecurityContextFactory.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/SecurityContextFactory.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/SecurityContextFactory.java
new file mode 100644
index 0000000..a0cac79
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/SecurityContextFactory.java
@@ -0,0 +1,45 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.Serializable;
+
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * How to create instances of a security context.
+ * 
+ * @author Donal Fellows
+ */
+public interface SecurityContextFactory extends Serializable {
+	/**
+	 * Creates a security context.
+	 * 
+	 * @param run
+	 *            Handle to remote run. Allows the security context to know how
+	 *            to apply itself to the workflow run.
+	 * @param owner
+	 *            The identity of the owner of the workflow run.
+	 * @return The security context.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	TavernaSecurityContext create(TavernaRun run, UsernamePrincipal owner)
+			throws Exception;
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/TavernaRun.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/TavernaRun.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/TavernaRun.java
new file mode 100644
index 0000000..8d9a7f8
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/TavernaRun.java
@@ -0,0 +1,232 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+
+/**
+ * The interface to a taverna workflow run, or "run" for short.
+ * 
+ * @author Donal Fellows
+ */
+public interface TavernaRun extends Serializable {
+	/**
+	 * @return The identifier of the run.
+	 */
+	String getId();
+
+	/**
+	 * @return What was this run was create to execute.
+	 */
+	Workflow getWorkflow();
+
+	/**
+	 * @return The name of the run.
+	 */
+	String getName();
+
+	/**
+	 * @param name
+	 *            The new name of the run. May be truncated.
+	 */
+	void setName(String name);
+
+	/**
+	 * @return The name of the Baclava file to use for all inputs, or
+	 *         <tt>null</tt> if no Baclava file is set.
+	 */
+	String getInputBaclavaFile();
+
+	/**
+	 * Sets the Baclava file to use for all inputs. This overrides the use of
+	 * individual inputs.
+	 * 
+	 * @param filename
+	 *            The filename to use. Must not start with a <tt>/</tt> or
+	 *            contain any <tt>..</tt> segments. Will be interpreted relative
+	 *            to the run's working directory.
+	 * @throws FilesystemAccessException
+	 *             If the filename is invalid.
+	 * @throws BadStateChangeException
+	 *             If the workflow is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	void setInputBaclavaFile(String filename) throws FilesystemAccessException,
+			BadStateChangeException;
+
+	/**
+	 * @return The list of input assignments.
+	 */
+	List<Input> getInputs();
+
+	/**
+	 * Create an input assignment.
+	 * 
+	 * @param name
+	 *            The name of the port that this will be an input for.
+	 * @return The assignment reference.
+	 * @throws BadStateChangeException
+	 *             If the workflow is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	Input makeInput(String name) throws BadStateChangeException;
+
+	/**
+	 * @return The file (relative to the working directory) to write the outputs
+	 *         of the run to as a Baclava document, or <tt>null</tt> if they are
+	 *         to be written to non-Baclava files in a directory called
+	 *         <tt>out</tt>.
+	 */
+	String getOutputBaclavaFile();
+
+	/**
+	 * Sets where the output of the run is to be written to. This will cause the
+	 * output to be generated as a Baclava document, rather than a collection of
+	 * individual non-Baclava files in the subdirectory of the working directory
+	 * called <tt>out</tt>.
+	 * 
+	 * @param filename
+	 *            Where to write the Baclava file (or <tt>null</tt> to cause the
+	 *            output to be written to individual files); overwrites any
+	 *            previous setting of this value.
+	 * @throws FilesystemAccessException
+	 *             If the filename starts with a <tt>/</tt> or contains a
+	 *             <tt>..</tt> segment.
+	 * @throws BadStateChangeException
+	 *             If the workflow is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	void setOutputBaclavaFile(String filename)
+			throws FilesystemAccessException, BadStateChangeException;
+
+	/**
+	 * @return When this run will expire, becoming eligible for automated
+	 *         deletion.
+	 */
+	Date getExpiry();
+
+	/**
+	 * Set when this run will expire.
+	 * 
+	 * @param d
+	 *            Expiry time. Deletion will happen some time after that.
+	 */
+	void setExpiry(Date d);
+
+	/**
+	 * @return The current status of the run.
+	 */
+	Status getStatus();
+
+	/**
+	 * Set the status of the run, which should cause it to move into the given
+	 * state. This may cause some significant changes.
+	 * 
+	 * @param s
+	 *            The state to try to change to.
+	 * @return <tt>null</tt>, or a string describing the incomplete state change
+	 *         if the operation has internally timed out.
+	 * @throws BadStateChangeException
+	 *             If the change to the given state is impossible.
+	 */
+	String setStatus(Status s) throws BadStateChangeException;
+
+	/**
+	 * @return Handle to the main working directory of the run.
+	 * @throws FilesystemAccessException
+	 */
+	Directory getWorkingDirectory() throws FilesystemAccessException;
+
+	/**
+	 * @return The list of listener instances attached to the run.
+	 */
+	List<Listener> getListeners();
+
+	/**
+	 * Add a listener to the run.
+	 * 
+	 * @param listener
+	 *            The listener to add.
+	 */
+	void addListener(Listener listener);
+
+	/**
+	 * @return The security context structure for this run.
+	 */
+	TavernaSecurityContext getSecurityContext();
+
+	/**
+	 * Kill off this run, removing all resources which it consumes.
+	 * 
+	 * @throws NoDestroyException
+	 *             If the destruction failed.
+	 */
+	void destroy() throws NoDestroyException;
+
+	/**
+	 * @return When this workflow run was created.
+	 */
+	Date getCreationTimestamp();
+
+	/**
+	 * @return When this workflow run was started, or <tt>null</tt> if it has
+	 *         never been started.
+	 */
+	Date getStartTimestamp();
+
+	/**
+	 * @return When this workflow run was found to have finished, or
+	 *         <tt>null</tt> if it has never finished (either still running or
+	 *         never started).
+	 */
+	Date getFinishTimestamp();
+
+	/**
+	 * Test if this run is really there.
+	 * 
+	 * <p>
+	 * <i>Implementation note:</i> Used to test communication fabrics, etc. so
+	 * implementations of this interface that do not delegate to another object
+	 * should do nothing.
+	 * 
+	 * @throws UnknownRunException
+	 *             If things fail.
+	 */
+	void ping() throws UnknownRunException;
+
+	/**
+	 * @return whether the run generates provenance data
+	 */
+	boolean getGenerateProvenance();
+
+	/**
+	 * @param generateProvenance
+	 *            whether the run generates provenance data
+	 */
+	void setGenerateProvenance(boolean generateProvenance);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/TavernaSecurityContext.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/TavernaSecurityContext.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/TavernaSecurityContext.java
new file mode 100644
index 0000000..3f993df
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/TavernaSecurityContext.java
@@ -0,0 +1,226 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.Principal;
+import java.util.Set;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.xml.ws.handler.MessageContext;
+
+import org.springframework.security.core.context.SecurityContext;
+import org.taverna.server.localworker.remote.ImplementationException;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Security context for a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+public interface TavernaSecurityContext {
+	/**
+	 * @return Who owns the security context.
+	 */
+	UsernamePrincipal getOwner();
+
+	/**
+	 * Describe the names of the users (as extracted from their
+	 * {@link Principal} objects) that may destroy the run or manipulate its
+	 * lifetime.
+	 * 
+	 * @return The names of the users who may use destroy operations. Read-only.
+	 */
+	Set<String> getPermittedDestroyers();
+
+	/**
+	 * Sets the collection of names of users (as extracted from their
+	 * {@link Principal} objects) that may destroy the run or manipulate its
+	 * lifetime.
+	 * 
+	 * @param destroyers
+	 *            The names of the users who may use destroy operations.
+	 */
+	void setPermittedDestroyers(Set<String> destroyers);
+
+	/**
+	 * Describe the names of the users (as extracted from their
+	 * {@link Principal} objects) that may update the run (including writing to
+	 * files).
+	 * 
+	 * @return The names of the users who may use update operations. Read-only.
+	 */
+	Set<String> getPermittedUpdaters();
+
+	/**
+	 * Sets the collection of names of users (as extracted from their
+	 * {@link Principal} objects) that may update the run (including writing to
+	 * its files).
+	 * 
+	 * @param updaters
+	 *            The names of the users who may use update operations.
+	 */
+	void setPermittedUpdaters(Set<String> updaters);
+
+	/**
+	 * Describe the names of the users (as extracted from their
+	 * {@link Principal} objects) that may read from the run (including its
+	 * files).
+	 * 
+	 * @return The names of the users who may use read operations. Read-only.
+	 */
+	Set<String> getPermittedReaders();
+
+	/**
+	 * Sets the collection of names of users (as extracted from their
+	 * {@link Principal} objects) that may read from the run (including its
+	 * files).
+	 * 
+	 * @param readers
+	 *            The names of the users who may use read operations.
+	 */
+	void setPermittedReaders(Set<String> readers);
+
+	/**
+	 * @return The credentials owned by the user. Never <tt>null</tt>.
+	 */
+	Credential[] getCredentials();
+
+	/**
+	 * Add a credential to the owned set or replaces the old version with the
+	 * new one.
+	 * 
+	 * @param toAdd
+	 *            The credential to add.
+	 */
+	void addCredential(Credential toAdd);
+
+	/**
+	 * Remove a credential from the owned set. It's not a failure to remove
+	 * something that isn't in the set.
+	 * 
+	 * @param toDelete
+	 *            The credential to remove.
+	 */
+	void deleteCredential(Credential toDelete);
+
+	/**
+	 * Tests if the credential is valid. This includes testing whether the
+	 * underlying credential file exists and can be unlocked by the password in
+	 * the {@link Credential} object.
+	 * 
+	 * @param c
+	 *            The credential object to validate.
+	 * @throws InvalidCredentialException
+	 *             If it is invalid.
+	 */
+	void validateCredential(Credential c) throws InvalidCredentialException;
+
+	/**
+	 * @return The identities trusted by the user. Never <tt>null</tt>.
+	 */
+	Trust[] getTrusted();
+
+	/**
+	 * Add an identity to the trusted set.
+	 * 
+	 * @param toAdd
+	 *            The identity to add.
+	 */
+	void addTrusted(Trust toAdd);
+
+	/**
+	 * Remove an identity from the trusted set. It's not a failure to remove
+	 * something that isn't in the set.
+	 * 
+	 * @param toDelete
+	 *            The identity to remove.
+	 */
+	void deleteTrusted(Trust toDelete);
+
+	/**
+	 * Tests if the trusted identity descriptor is valid. This includes checking
+	 * whether the underlying trusted identity file exists.
+	 * 
+	 * @param t
+	 *            The trusted identity descriptor to check.
+	 * @throws InvalidCredentialException
+	 *             If it is invalid.
+	 */
+	void validateTrusted(Trust t) throws InvalidCredentialException;
+
+	/**
+	 * Establish the security context from how the owning workflow run was
+	 * created. In particular, this gives an opportunity for boot-strapping
+	 * things with any delegateable credentials.
+	 * 
+	 * @param securityContext
+	 *            The security context associated with the request that caused
+	 *            the workflow to be created.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	void initializeSecurityFromContext(SecurityContext securityContext)
+			throws Exception;
+
+	/**
+	 * Establish the security context from how the owning workflow run was
+	 * created. In particular, this gives an opportunity for boot-strapping
+	 * things with any delegateable credentials.
+	 * 
+	 * @param context
+	 *            The full information about the request that caused the
+	 *            workflow to be created.
+	 */
+	void initializeSecurityFromSOAPContext(MessageContext context);
+
+	/**
+	 * Establish the security context from how the owning workflow run was
+	 * created. In particular, this gives an opportunity for boot-strapping
+	 * things with any delegateable credentials.
+	 * 
+	 * @param headers
+	 *            The full information about the request that caused the
+	 *            workflow to be created.
+	 */
+	void initializeSecurityFromRESTContext(HttpHeaders headers);
+
+	/**
+	 * Transfer the security context to the remote system.
+	 * 
+	 * @throws IOException
+	 *             If the communication fails.
+	 * @throws GeneralSecurityException
+	 *             If the assembly of the context fails.
+	 * @throws ImplementationException
+	 *             If the local worker has problems with creating the realized
+	 *             security context.
+	 */
+	void conveySecurity() throws GeneralSecurityException, IOException,
+			ImplementationException;
+
+	/**
+	 * @return The factory that created this security context.
+	 */
+	SecurityContextFactory getFactory();
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/UriBuilderFactory.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/UriBuilderFactory.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/UriBuilderFactory.java
new file mode 100644
index 0000000..c4d0fb5
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/UriBuilderFactory.java
@@ -0,0 +1,56 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.net.URI;
+
+import javax.ws.rs.core.UriBuilder;
+
+/**
+ * How to manufacture URIs to workflow runs.
+ * 
+ * @author Donal Fellows
+ */
+public interface UriBuilderFactory {
+	/**
+	 * Given a run, get a factory for RESTful URIs to resources associated
+	 * with it.
+	 * 
+	 * @param run
+	 *            The run in question.
+	 * @return The {@link URI} factory.
+	 */
+	UriBuilder getRunUriBuilder(TavernaRun run);
+
+	/**
+	 * @return a URI factory that is preconfigured to point to the base of
+	 *         the webapp.
+	 */
+	UriBuilder getBaseUriBuilder();
+
+	/**
+	 * Resolves a URI with respect to the base URI of the factory.
+	 * 
+	 * @param uri
+	 *            The URI to resolve, or <tt>null</tt>.
+	 * @return The resolved URI, or <tt>null</tt> if <b>uri</b> is
+	 *         <tt>null</tt>.
+	 */
+	String resolve(String uri);
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/package-info.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/package-info.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/package-info.java
new file mode 100644
index 0000000..9c9b5b8
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/package-info.java
@@ -0,0 +1,23 @@
+/*
+ */
+/**
+ * Interfaces to the main worker classes that provide the magical power
+ * that drives the webapp front-end.
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/AbstractRemoteRunFactory.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/AbstractRemoteRunFactory.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/AbstractRemoteRunFactory.java
new file mode 100644
index 0000000..72004b4
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/AbstractRemoteRunFactory.java
@@ -0,0 +1,452 @@
+/*
+ */
+package org.taverna.server.master.localworker;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static java.lang.System.getSecurityManager;
+import static java.lang.System.setProperty;
+import static java.lang.System.setSecurityManager;
+import static java.rmi.registry.LocateRegistry.createRegistry;
+import static java.rmi.registry.LocateRegistry.getRegistry;
+import static java.rmi.registry.Registry.REGISTRY_PORT;
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.DIR;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.net.URI;
+import java.net.URL;
+import java.rmi.MarshalledObject;
+import java.rmi.RemoteException;
+import java.rmi.registry.LocateRegistry;
+import java.rmi.registry.Registry;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.Resource;
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.io.IOUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.taverna.server.localworker.remote.RemoteRunFactory;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.localworker.server.UsageRecordReceiver;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.factories.ListenerFactory;
+import org.taverna.server.master.factories.RunFactory;
+import org.taverna.server.master.interaction.InteractionFeedSupport;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.SecurityContextFactory;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.notification.atom.EventDAO;
+import org.taverna.server.master.usage.UsageRecordRecorder;
+import org.taverna.server.master.utils.UsernamePrincipal;
+import org.taverna.server.master.worker.FactoryBean;
+import org.taverna.server.master.worker.RemoteRunDelegate;
+import org.taverna.server.master.worker.RunFactoryConfiguration;
+
+import org.apache.taverna.scufl2.api.io.WriterException;
+
+/**
+ * Bridge to remote runs via RMI.
+ * 
+ * @author Donal Fellows
+ */
+@ManagedResource(objectName = JMX_ROOT + "Factory", description = "The factory for runs")
+public abstract class AbstractRemoteRunFactory extends RunFactoryConfiguration
+		implements ListenerFactory, RunFactory, FactoryBean {
+	/**
+	 * Whether to apply stronger limitations than normal to RMI. It is
+	 * recommended that this be true!
+	 */
+	@Value("${rmi.localhostOnly}")
+	private boolean rmiLocalhostOnly;
+	/** The interaction host name. */
+	private String interhost;
+	/** The interaction port number. */
+	private String interport;
+	private Process registryProcess;
+	/**
+	 * The interaction WebDAV location. Will be resolved before being passed to
+	 * the back-end.
+	 */
+	private String interwebdav;
+	/**
+	 * The interaction ATOM feed location. Will be resolved before being passed
+	 * to the back-end.
+	 */
+	private String interfeed;
+	/** Used for doing URI resolution. */
+	@Resource(name = "webapp")
+	private UriBuilderFactory baseurifactory;
+	@Autowired
+	private InteractionFeedSupport interactionFeedSupport;
+
+	@Value("${taverna.interaction.host}")
+	void setInteractionHost(String host) {
+		if (host != null && host.equals("none"))
+			host = null;
+		interhost = host;
+	}
+
+	@Value("${taverna.interaction.port}")
+	void setInteractionPort(String port) {
+		if (port != null && port.equals("none"))
+			port = null;
+		interport = port;
+	}
+
+	@Value("${taverna.interaction.webdav_path}")
+	void setInteractionWebdav(String webdav) {
+		if (webdav != null && webdav.equals("none"))
+			webdav = null;
+		interwebdav = webdav;
+	}
+
+	@Value("${taverna.interaction.feed_path}")
+	void setInteractionFeed(String feed) {
+		if (feed != null && feed.equals("none"))
+			feed = null;
+		interfeed = feed;
+	}
+
+	@Override
+	protected void reinitRegistry() {
+		registry = null;
+		if (registryProcess != null) {
+			registryProcess.destroy();
+			registryProcess = null;
+		}
+	}
+
+	protected void initInteractionDetails(RemoteRunFactory factory)
+			throws RemoteException {
+		if (interhost != null) {
+			String feed = baseurifactory.resolve(interfeed);
+			String webdav = baseurifactory.resolve(interwebdav);
+			factory.setInteractionServiceDetails(interhost, interport, webdav,
+					feed);
+		}
+	}
+
+	protected static final Process launchSubprocess(ProcessBuilder b)
+			throws IOException {
+		Thread t = Thread.currentThread();
+		ClassLoader ccl = t.getContextClassLoader();
+		try {
+			t.setContextClassLoader(null);
+			return b.start();
+		} finally {
+			t.setContextClassLoader(ccl);
+		}
+	}
+
+	/** Get a handle to a new instance of the RMI registry. */
+	private Registry makeRegistry(int port) throws RemoteException {
+		ProcessBuilder p = new ProcessBuilder(getJavaBinary());
+		p.command().add("-jar");
+		p.command().add(getRmiRegistryJar());
+		p.command().add(Integer.toString(port));
+		p.command().add(Boolean.toString(rmiLocalhostOnly));
+		try {
+			Process proc = launchSubprocess(p);
+			Thread.sleep(getSleepTime());
+			try {
+				if (proc.exitValue() == 0)
+					return null;
+				String error = IOUtils.toString(proc.getErrorStream());
+				throw new RemoteException(error);
+			} catch (IllegalThreadStateException ise) {
+				// Still running!
+			}
+			try (ObjectInputStream ois = new ObjectInputStream(
+					proc.getInputStream())) {
+				@SuppressWarnings("unchecked")
+				Registry r = ((MarshalledObject<Registry>) ois.readObject())
+						.get();
+				registryProcess = proc;
+				return r;
+			}
+		} catch (RemoteException e) {
+			throw e;
+		} catch (ClassNotFoundException e) {
+			throw new RemoteException("unexpected registry type", e);
+		} catch (IOException e) {
+			throw new RemoteException("unexpected IO problem with registry", e);
+		} catch (InterruptedException e) {
+			throw new RemoteException("unexpected interrupt");
+		}
+	}
+
+	/**
+	 * @return A handle to the current RMI registry.
+	 */
+	protected Registry getTheRegistry() {
+		try {
+			if (registry != null) {
+				registry.list();
+				return registry;
+			}
+		} catch (RemoteException e) {
+			log.warn("non-functioning existing registry handle", e);
+			registry = null;
+		}
+		try {
+			registry = getRegistry(getRegistryHost(), getRegistryPort());
+			registry.list();
+			return registry;
+		} catch (RemoteException e) {
+			log.warn("Failed to get working RMI registry handle.");
+			registry = null;
+			log.warn("Will build new registry, "
+					+ "but service restart ability is at risk.");
+			try {
+				registry = makeRegistry(getRegistryPort());
+				registry.list();
+				return registry;
+			} catch (RemoteException e2) {
+				log.error(
+						"failed to create local working RMI registry on port "
+								+ getRegistryPort(), e2);
+				log.info("original connection exception", e);
+			}
+		}
+		try {
+			registry = getRegistry(getRegistryHost(), REGISTRY_PORT);
+			registry.list();
+			return registry;
+		} catch (RemoteException e) {
+			log.warn("Failed to get working RMI registry handle on backup port.");
+			try {
+				registry = makeRegistry(REGISTRY_PORT);
+				registry.list();
+				return registry;
+			} catch (RemoteException e2) {
+				log.fatal(
+						"totally failed to get registry handle, even on fallback!",
+						e2);
+				log.info("original connection exception", e);
+				registry = null;
+				throw new RuntimeException("No RMI Registry Available");
+			}
+		}
+	}
+
+	private Registry registry;
+	/**
+	 * The name of the resource that describes the default security policy to
+	 * install.
+	 */
+	public static final String SECURITY_POLICY_FILE = "security.policy";
+	private SecurityContextFactory securityFactory;
+	UsageRecordRecorder usageRecordSink;
+	private EventDAO masterEventFeed;
+
+	@Autowired(required = true)
+	void setSecurityContextFactory(SecurityContextFactory factory) {
+		this.securityFactory = factory;
+	}
+
+	@Autowired(required = true)
+	void setMasterEventFeed(EventDAO masterEventFeed) {
+		this.masterEventFeed = masterEventFeed;
+	}
+
+	@Autowired(required = true)
+	void setUsageRecordSink(UsageRecordRecorder usageRecordSink) {
+		this.usageRecordSink = usageRecordSink;
+	}
+
+	/**
+	 * Configures the Java security model. Not currently used, as it is
+	 * viciously difficult to get right!
+	 */
+	@SuppressWarnings("unused")
+	private static void installSecurityManager() {
+		if (getSecurityManager() == null) {
+			setProperty("java.security.policy", AbstractRemoteRunFactory.class
+					.getClassLoader().getResource(SECURITY_POLICY_FILE)
+					.toExternalForm());
+			setSecurityManager(new SecurityManager());
+		}
+	}
+
+	// static {
+	// installSecurityManager();
+	// }
+
+	/**
+	 * Set up the run expiry management engine.
+	 * 
+	 * @throws JAXBException
+	 */
+	public AbstractRemoteRunFactory() throws JAXBException {
+		try {
+			registry = LocateRegistry.getRegistry();
+			registry.list();
+		} catch (RemoteException e) {
+			log.warn("Failed to get working RMI registry handle.");
+			log.warn("Will build new registry, but service restart ability is at risk.");
+			try {
+				registry = createRegistry(REGISTRY_PORT);
+				registry.list();
+			} catch (RemoteException e2) {
+				log.error("failed to create working RMI registry", e2);
+				log.info("original connection exception", e);
+			}
+		}
+	}
+
+	@Override
+	public List<String> getSupportedListenerTypes() {
+		try {
+			RemoteRunDelegate rrd = runDB.pickArbitraryRun();
+			if (rrd != null)
+				return rrd.getListenerTypes();
+			log.warn("no remote runs; no listener types");
+		} catch (Exception e) {
+			log.warn("failed to get list of listener types", e);
+		}
+		return new ArrayList<>();
+	}
+
+	@Override
+	public Listener makeListener(TavernaRun run, String listenerType,
+			String configuration) throws NoListenerException {
+		if (run instanceof RemoteRunDelegate)
+			return ((RemoteRunDelegate) run).makeListener(listenerType,
+					configuration);
+		throw new NoListenerException("unexpected run type: " + run.getClass());
+	}
+
+	@Override
+	public TavernaRun create(UsernamePrincipal creator, Workflow workflow)
+			throws NoCreateException {
+		try {
+			Date now = new Date();
+			UUID id = randomUUID();
+			RemoteSingleRun rsr = getRealRun(creator, workflow, id);
+			RemoteRunDelegate run = new RemoteRunDelegate(now, workflow, rsr,
+					state.getDefaultLifetime(), runDB, id,
+					state.getGenerateProvenance(), this);
+			run.setSecurityContext(securityFactory.create(run, creator));
+			@Nonnull
+			URI feed = interactionFeedSupport.getFeedURI(run);
+			@Nonnull
+			URL feedUrl = feed.toURL();
+			@Nonnull
+			URL webdavUrl = baseurifactory.getRunUriBuilder(run)
+					.path(DIR + "/interactions").build().toURL();
+			@Nullable
+			URL pub = interactionFeedSupport.getLocalFeedBase(feed);
+			rsr.setInteractionServiceDetails(feedUrl, webdavUrl, pub);
+			return run;
+		} catch (NoCreateException e) {
+			log.warn("failed to build run instance", e);
+			throw e;
+		} catch (Exception e) {
+			log.warn("failed to build run instance", e);
+			throw new NoCreateException("failed to build run instance", e);
+		}
+	}
+
+	/**
+	 * Gets the RMI connector for a new run.
+	 * 
+	 * @param creator
+	 *            Who is creating the workflow run.
+	 * @param workflow
+	 *            What workflow are they instantiating.
+	 * @param id
+	 *            The identity token for the run, newly minted.
+	 * @return The remote interface to the run.
+	 * @throws Exception
+	 *             Just about anything can go wrong...
+	 */
+	protected abstract RemoteSingleRun getRealRun(UsernamePrincipal creator,
+			Workflow workflow, UUID id) throws Exception;
+
+	/**
+	 * How to convert a wrapped workflow into XML.
+	 * 
+	 * @param workflow
+	 *            The wrapped workflow.
+	 * @return The XML version of the document.
+	 * @throws JAXBException
+	 *             If serialization fails.
+	 */
+	protected byte[] serializeWorkflow(Workflow workflow) throws JAXBException {
+		try {
+			return workflow.getScufl2Bytes();
+		} catch (IOException e) {
+			throw new JAXBException("problem converting to scufl2", e);
+		} catch (WriterException e) {
+			throw new JAXBException("problem converting to scufl2", e);
+		}
+	}
+
+	private void acceptUsageRecord(String usageRecord) {
+		if (usageRecordSink != null)
+			usageRecordSink.storeUsageRecord(usageRecord);
+		runDB.checkForFinishNow();
+	}
+
+	/**
+	 * Make a Remote object that can act as a consumer for usage records.
+	 * 
+	 * @param creator
+	 * 
+	 * @return The receiver, or <tt>null</tt> if the construction fails.
+	 */
+	protected UsageRecordReceiver makeURReciver(UsernamePrincipal creator) {
+		try {
+			@SuppressWarnings("serial")
+			class URReceiver extends UnicastRemoteObject implements
+					UsageRecordReceiver {
+				public URReceiver() throws RemoteException {
+					super();
+				}
+
+				@Override
+				public void acceptUsageRecord(String usageRecord) {
+					AbstractRemoteRunFactory.this.acceptUsageRecord(usageRecord);
+				}
+			}
+			return new URReceiver();
+		} catch (RemoteException e) {
+			log.warn("failed to build usage record receiver", e);
+			return null;
+		}
+	}
+
+	@Override
+	public EventDAO getMasterEventFeed() {
+		return masterEventFeed;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/ForkRunFactory.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/ForkRunFactory.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/ForkRunFactory.java
new file mode 100644
index 0000000..3f48644
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/ForkRunFactory.java
@@ -0,0 +1,336 @@
+/*
+ */
+package org.taverna.server.master.localworker;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static java.lang.System.getProperty;
+import static java.lang.Thread.sleep;
+import static java.util.Arrays.asList;
+import static java.util.Calendar.SECOND;
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+
+import java.io.File;
+import java.rmi.ConnectException;
+import java.rmi.ConnectIOException;
+import java.rmi.NotBoundException;
+import java.rmi.RemoteException;
+import java.util.Calendar;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.xml.bind.JAXBException;
+
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.taverna.server.localworker.remote.RemoteRunFactory;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.factories.ConfigurableRunFactory;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * A simple factory for workflow runs that forks runs from a subprocess.
+ * 
+ * @author Donal Fellows
+ */
+@ManagedResource(objectName = JMX_ROOT + "RunFactory", description = "The factory for simple singleton forked run.")
+public class ForkRunFactory extends AbstractRemoteRunFactory implements
+		ConfigurableRunFactory {
+	private int lastStartupCheckCount;
+	private Integer lastExitCode;
+	private RemoteRunFactory factory;
+	private Process factoryProcess;
+	private String factoryProcessName;
+
+	/**
+	 * Create a factory for remote runs that works by forking off a subprocess.
+	 * 
+	 * @throws JAXBException
+	 *             Shouldn't happen.
+	 */
+	public ForkRunFactory() throws JAXBException {
+	}
+
+	@PostConstruct
+	protected void initRegistry() {
+		log.info("waiting for availability of default RMI registry");
+		getTheRegistry();
+	}
+
+	@Override
+	protected void reinitFactory() {
+		boolean makeFactory = factory != null;
+		killFactory();
+		try {
+			if (makeFactory)
+				initFactory();
+		} catch (Exception e) {
+			log.fatal("failed to make connection to remote run factory", e);
+		}
+	}
+
+	private RemoteRunFactory getFactory() throws RemoteException {
+		try {
+			initFactory();
+		} catch (RemoteException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new RemoteException("problem constructing factory", e);
+		}
+		return factory;
+	}
+
+	/**
+	 * @return How many checks were done for the worker process the last time a
+	 *         spawn was tried.
+	 */
+	@ManagedAttribute(description = "How many checks were done for the worker process the last time a spawn was tried.", currencyTimeLimit = 60)
+	@Override
+	public int getLastStartupCheckCount() {
+		return lastStartupCheckCount;
+	}
+
+	/**
+	 * @return What was the exit code from the last time the factory subprocess
+	 *         was killed?
+	 */
+	@ManagedAttribute(description = "What was the exit code from the last time the factory subprocess was killed?")
+	@Override
+	public Integer getLastExitCode() {
+		return lastExitCode;
+	}
+
+	/**
+	 * @return What the factory subprocess's main RMI interface is registered
+	 *         as.
+	 */
+	@ManagedAttribute(description = "What the factory subprocess's main RMI interface is registered as.", currencyTimeLimit = 60)
+	@Override
+	public String getFactoryProcessName() {
+		return factoryProcessName;
+	}
+
+	/**
+	 * Makes the subprocess that manufactures runs.
+	 * 
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	public void initFactory() throws Exception {
+		if (factory != null)
+			return;
+		// Generate the arguments to use when spawning the subprocess
+		factoryProcessName = state.getFactoryProcessNamePrefix() + randomUUID();
+		ProcessBuilder p = new ProcessBuilder(getJavaBinary());
+		p.command().add("-jar");
+		p.command().add(getServerWorkerJar());
+		if (getExecuteWorkflowScript() == null)
+			log.fatal("no execute workflow script");
+		p.command().add(getExecuteWorkflowScript());
+		p.command().addAll(asList(getExtraArguments()));
+		p.command().add(factoryProcessName);
+		p.redirectErrorStream(true);
+		p.directory(new File(getProperty("javax.servlet.context.tempdir",
+				getProperty("java.io.tmpdir"))));
+
+		// Spawn the subprocess
+		log.info("about to create subprocess: " + p.command());
+		
+		factoryProcess = launchSubprocess(p);
+		outlog = new StreamLogger("FactoryStdout", factoryProcess.getInputStream()) {
+			@Override
+			protected void write(String msg) {
+				log.info(msg);
+			}
+		};
+		errlog = new StreamLogger("FactoryStderr", factoryProcess.getErrorStream()) {
+			@Override
+			protected void write(String msg) {
+				log.info(msg);
+			}
+		};
+
+		// Wait for the subprocess to register itself in the RMI registry
+		Calendar deadline = Calendar.getInstance();
+		deadline.add(SECOND, state.getWaitSeconds());
+		Exception lastException = null;
+		lastStartupCheckCount = 0;
+		while (deadline.after(Calendar.getInstance())) {
+			try {
+				sleep(state.getSleepMS());
+				lastStartupCheckCount++;
+				factory = getRemoteFactoryHandle(factoryProcessName);
+				initInteractionDetails(factory);
+				return;
+			} catch (InterruptedException ie) {
+				continue;
+			} catch (NotBoundException nbe) {
+				lastException = nbe;
+				log.info("resource \"" + factoryProcessName
+						+ "\" not yet registered...");
+				continue;
+			} catch (RemoteException re) {
+				// Unpack a remote exception if we can
+				lastException = re;
+				try {
+					if (re.getCause() != null)
+						lastException = (Exception) re.getCause();
+				} catch (Throwable t) {
+					// Ignore!
+				}
+			} catch (RuntimeException e) {
+				lastException = e;
+			}
+		}
+		if (lastException == null)
+			lastException = new InterruptedException();
+		throw lastException;
+	}
+
+	private StreamLogger outlog, errlog;
+
+	private void stopLoggers() {
+		if (outlog != null)
+			outlog.stop();
+		outlog = null;
+		if (errlog != null)
+			errlog.stop();
+		errlog = null;
+	}
+
+	private RemoteRunFactory getRemoteFactoryHandle(String name)
+			throws RemoteException, NotBoundException {
+		log.info("about to look up resource called " + name);
+		try {
+			// Validate registry connection first
+			getTheRegistry().list();
+		} catch (ConnectException | ConnectIOException e) {
+			log.warn("connection problems with registry", e);
+		}
+		RemoteRunFactory rrf = (RemoteRunFactory) getTheRegistry().lookup(name);
+		log.info("successfully connected to factory subprocess "
+				+ factoryProcessName);
+		return rrf;
+	}
+
+	/**
+	 * Destroys the subprocess that manufactures runs.
+	 */
+	@PreDestroy
+	public void killFactory() {
+		if (factory != null) {
+			log.info("requesting shutdown of " + factoryProcessName);
+			try {
+				factory.shutdown();
+				sleep(700);
+			} catch (RemoteException e) {
+				log.warn(factoryProcessName + " failed to shut down nicely", e);
+			} catch (InterruptedException e) {
+				if (log.isDebugEnabled())
+					log.debug("interrupted during wait after asking "
+							+ factoryProcessName + " to shut down", e);
+			} finally {
+				factory = null;
+			}
+		}
+
+		if (factoryProcess != null) {
+			int code = -1;
+			try {
+				lastExitCode = code = factoryProcess.exitValue();
+				log.info(factoryProcessName + " already dead?");
+			} catch (RuntimeException e) {
+				log.info("trying to force death of " + factoryProcessName);
+				try {
+					factoryProcess.destroy();
+					sleep(350); // takes a little time, even normally
+					lastExitCode = code = factoryProcess.exitValue();
+				} catch (Exception e2) {
+					code = -1;
+				}
+			} finally {
+				factoryProcess = null;
+				stopLoggers();
+			}
+			if (code > 128) {
+				log.info(factoryProcessName + " died with signal="
+						+ (code - 128));
+			} else if (code >= 0) {
+				log.info(factoryProcessName + " process killed: code=" + code);
+			} else {
+				log.warn(factoryProcessName + " not yet dead");
+			}
+		}
+	}
+
+	/**
+	 * The real core of the run builder, factored out from its reliability
+	 * support.
+	 * 
+	 * @param creator
+	 *            Who created this workflow?
+	 * @param wf
+	 *            The serialized workflow.
+	 * @return The remote handle of the workflow run.
+	 * @throws RemoteException
+	 *             If anything fails (communications error, etc.)
+	 */
+	private RemoteSingleRun getRealRun(@Nonnull UsernamePrincipal creator,
+			@Nonnull byte[] wf, UUID id) throws RemoteException {
+		@Nonnull
+		String globaluser = "Unknown Person";
+		if (creator != null)
+			globaluser = creator.getName();
+		RemoteSingleRun rsr = getFactory().make(wf, globaluser,
+				makeURReciver(creator), id);
+		incrementRunCount();
+		return rsr;
+	}
+
+	@Override
+	protected RemoteSingleRun getRealRun(UsernamePrincipal creator,
+			Workflow workflow, UUID id) throws Exception {
+		@Nonnull
+		byte[] wf = serializeWorkflow(workflow);
+		for (int i = 0; i < 3; i++) {
+			initFactory();
+			try {
+				return getRealRun(creator, wf, id);
+			} catch (ConnectException | ConnectIOException e) {
+				// factory was lost; try to recreate
+			}
+			killFactory();
+		}
+		throw new NoCreateException("total failure to connect to factory "
+				+ factoryProcessName + "despite attempting restart");
+	}
+
+	@Override
+	public String[] getFactoryProcessMapping() {
+		return new String[0];
+	}
+
+	@Override
+	protected int operatingCount() throws Exception {
+		return getFactory().countOperatingRuns();
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/IdAwareForkRunFactory.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/IdAwareForkRunFactory.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/IdAwareForkRunFactory.java
new file mode 100644
index 0000000..a2e5ff7
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/IdAwareForkRunFactory.java
@@ -0,0 +1,529 @@
+/*
+ */
+package org.taverna.server.master.localworker;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static java.lang.System.getProperty;
+import static java.lang.Thread.sleep;
+import static java.util.Arrays.asList;
+import static java.util.Calendar.SECOND;
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+import static org.taverna.server.master.localworker.AbstractRemoteRunFactory.launchSubprocess;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.rmi.ConnectException;
+import java.rmi.ConnectIOException;
+import java.rmi.NotBoundException;
+import java.rmi.RemoteException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.annotation.Order;
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.taverna.server.localworker.remote.RemoteRunFactory;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.factories.ConfigurableRunFactory;
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * A simple factory for workflow runs that forks runs from a subprocess.
+ * 
+ * @author Donal Fellows
+ */
+@ManagedResource(objectName = JMX_ROOT + "RunFactory", description = "The factory for a user-specific forked run.")
+public class IdAwareForkRunFactory extends AbstractRemoteRunFactory implements
+		ConfigurableRunFactory {
+	private MetaFactory forker;
+	private Map<String, RemoteRunFactory> factory;
+	private Map<String, String> factoryProcessName;
+
+	/**
+	 * Create a factory for remote runs that works by forking off a subprocess.
+	 * 
+	 * @throws JAXBException
+	 *             Shouldn't happen.
+	 */
+	public IdAwareForkRunFactory() throws JAXBException {
+		factory = new HashMap<>();
+		factoryProcessName = new HashMap<>();
+	}
+
+	@Override
+	protected void reinitFactory() {
+		boolean makeForker = forker != null;
+		try {
+			killForker();
+		} catch (Exception e) {
+			log.warn("exception when killing secure-fork process", e);
+		}
+		try {
+			if (makeForker)
+				initMetaFactory();
+		} catch (Exception e) {
+			log.fatal("failed to make secure-fork process", e);
+		}
+	}
+
+	/**
+	 * @return How many checks were done for the worker process the last time a
+	 *         spawn was tried.
+	 */
+	@Override
+	@ManagedAttribute(description = "How many checks were done for the worker process the last time a spawn was tried.", currencyTimeLimit = 60)
+	public int getLastStartupCheckCount() {
+		return forker == null ? 0 : forker.lastStartupCheckCount();
+	}
+
+	/**
+	 * @return What was the exit code from the last time the factory subprocess
+	 *         was killed?
+	 */
+	@Override
+	@ManagedAttribute(description = "What was the exit code from the last time the factory subprocess was killed?")
+	public Integer getLastExitCode() {
+		return forker == null ? null : forker.lastExitCode();
+	}
+
+	/**
+	 * @return The mapping of user names to RMI factory IDs.
+	 */
+	@Override
+	@ManagedAttribute(description = "The mapping of user names to RMI factory IDs.", currencyTimeLimit = 60)
+	public String[] getFactoryProcessMapping() {
+		ArrayList<String> result = new ArrayList<>();
+		ArrayList<String> keys = new ArrayList<>(factoryProcessName.keySet());
+		String[] ks = keys.toArray(new String[keys.size()]);
+		Arrays.sort(ks);
+		for (String k : ks) {
+			result.add(k);
+			result.add(factoryProcessName.get(k));
+		}
+		return result.toArray(new String[result.size()]);
+	}
+
+	/**
+	 * How construction of factories is actually done.
+	 * 
+	 * @author Donal Fellows
+	 */
+	public interface MetaFactory {
+		/**
+		 * Make a factory for the given user.
+		 * 
+		 * @param username
+		 *            Who to make it for.
+		 * @return Handle of the factory.
+		 * @throws Exception
+		 *             If anything goes wrong.
+		 */
+		RemoteRunFactory make(String username) throws Exception;
+
+		/**
+		 * Shut down the meta-factory. It is not defined whether factories
+		 * created by it are also shut down at the same time.
+		 * 
+		 * @throws IOException
+		 *             If something goes wrong when communicating with the
+		 *             meta-factory.
+		 * @throws InterruptedException
+		 *             If something stops us waiting for the shut down to
+		 *             happen.
+		 */
+		void close() throws IOException, InterruptedException;
+
+		int lastStartupCheckCount();
+
+		Integer lastExitCode();
+	}
+
+	void registerFactory(String username, String fpn, RemoteRunFactory f) {
+		factoryProcessName.put(username, fpn);
+		factory.put(username, f);
+	}
+
+	/**
+	 * Makes the connection to the meta-factory that makes factories.
+	 * 
+	 * @throws IOException
+	 *             If the connection fails.
+	 */
+	@PostConstruct
+	void initMetaFactory() throws IOException {
+		log.info("waiting for availability of default RMI registry");
+		getTheRegistry();
+		log.info("constructing secure fork subprocess");
+		forker = new SecureFork(this, state, log);
+	}
+
+	private void killForker() throws IOException, InterruptedException {
+		try {
+			if (forker != null)
+				forker.close();
+		} finally {
+			forker = null;
+		}
+	}
+
+	/**
+	 * Makes the subprocess that manufactures runs.
+	 * 
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	private void initFactory(String username) throws Exception {
+		if (factory.containsKey(username))
+			return;
+		if (forker == null)
+			initMetaFactory();
+		forker.make(username);
+	}
+
+	/**
+	 * Destroys the subprocess that manufactures runs.
+	 */
+	@PreDestroy
+	public void killFactories() {
+		if (!factory.isEmpty()) {
+			Iterator<String> keys = factory.keySet().iterator();
+			while (keys.hasNext()) {
+				String key = keys.next();
+				log.info("requesting shutdown of "
+						+ factoryProcessName.get(key));
+				try {
+					factory.get(key).shutdown();
+				} catch (RemoteException e) {
+					log.warn(factoryProcessName.get(key)
+							+ " failed to shut down nicely", e);
+				} finally {
+					keys.remove();
+					factoryProcessName.remove(key);
+				}
+			}
+			try {
+				sleep(700);
+			} catch (InterruptedException e) {
+				if (log.isDebugEnabled())
+					log.debug("interrupted during wait after "
+							+ "asking factories to shut down", e);
+			}
+		}
+
+		try {
+			killForker();
+		} catch (Exception e) {
+			if (log.isDebugEnabled())
+				log.debug("exception in shutdown of secure-fork process", e);
+		}
+	}
+
+	@Override
+	protected void finalize() throws Throwable {
+		killFactories();
+		super.finalize();
+	}
+
+	@Autowired
+	public void setIdMapper(LocalIdentityMapper mapper) {
+		this.mapper = mapper;
+	}
+
+	private LocalIdentityMapper mapper;
+
+	/**
+	 * The real core of the run builder, factored out from its reliability
+	 * support.
+	 * 
+	 * @param creator
+	 *            Who created this workflow?
+	 * @param username
+	 *            What user account is this workflow to be executed in?
+	 * @param wf
+	 *            The serialized workflow.
+	 * @return The remote handle of the workflow run.
+	 * @throws RemoteException
+	 *             If anything fails (communications error, etc.)
+	 */
+	private RemoteSingleRun getRealRun(@Nonnull UsernamePrincipal creator,
+			@Nonnull String username, @Nonnull byte[] wf, UUID id)
+			throws RemoteException {
+		String globaluser = "Unknown Person";
+		if (creator != null)
+			globaluser = creator.getName();
+		RemoteSingleRun rsr = factory.get(username).make(wf, globaluser,
+				makeURReciver(creator), id);
+		incrementRunCount();
+		return rsr;
+	}
+
+	@Override
+	protected RemoteSingleRun getRealRun(UsernamePrincipal creator,
+			Workflow workflow, UUID id) throws Exception {
+		byte[] wf = serializeWorkflow(workflow);
+		String username = mapper == null ? null : mapper
+				.getUsernameForPrincipal(creator);
+		if (username == null)
+			throw new Exception("cannot determine who to run workflow as; "
+					+ "local identity mapper returned null");
+		for (int i = 0; i < 3; i++) {
+			if (!factory.containsKey(username))
+				initFactory(username);
+			try {
+				return getRealRun(creator, username, wf, id);
+			} catch (ConnectException | ConnectIOException e) {
+				// factory was lost; try to recreate
+			}
+			factory.remove(username);
+		}
+		throw new NoCreateException("total failure to connect to factory "
+				+ factoryProcessName + "despite attempting restart");
+	}
+
+	@Value("${secureForkPasswordFile}")
+	@Order(20)
+	public void setPasswordSource(String passwordSource) {
+		if (passwordSource == null || passwordSource.isEmpty()
+				|| passwordSource.startsWith("${"))
+			state.setDefaultPasswordFile(null);
+		else
+			state.setDefaultPasswordFile(passwordSource);
+		if (state.getPasswordFile() == null)
+			log.info("assuming password-free forking enabled");
+		else
+			log.info("configured secureForkPasswordFile from context as "
+					+ state.getPasswordFile());
+	}
+
+	@Override
+	public String getFactoryProcessName() {
+		return "<PROPERTY-NOT-SUPPORTED>";
+	}
+
+	@Override
+	protected int operatingCount() throws Exception {
+		int total = 0;
+		for (RemoteRunFactory rrf : factory.values())
+			total += rrf.countOperatingRuns();
+		return total;
+	}
+}
+
+/**
+ * The connector that handles the secure fork process itself.
+ * 
+ * @author Donal Fellows
+ */
+class SecureFork implements IdAwareForkRunFactory.MetaFactory {
+	private IdAwareForkRunFactory main;
+	private Process process;
+	private PrintWriter channel;
+	private int lastStartupCheckCount;
+	private Integer lastExitCode;
+	private Log log;
+	private LocalWorkerState state;
+	private StreamLogger out, err;
+
+	/**
+	 * Construct the command to run the meta-factory process.
+	 * 
+	 * @param args
+	 *            The live list of arguments to pass.
+	 */
+	public void initFactoryArgs(List<String> args) {
+		args.add(main.getJavaBinary());
+		String pwf = main.getPasswordFile();
+		if (pwf != null) {
+			args.add("-Dpassword.file=" + pwf);
+		}
+		args.add("-jar");
+		args.add(main.getServerForkerJar());
+		args.add(main.getJavaBinary());
+		args.add("-jar");
+		args.add(main.getServerWorkerJar());
+		if (main.getExecuteWorkflowScript() == null)
+			log.fatal("no execute workflow script");
+		args.add(main.getExecuteWorkflowScript());
+		args.addAll(asList(main.getExtraArguments()));
+	}
+
+	SecureFork(IdAwareForkRunFactory main, LocalWorkerState state, Log log)
+			throws IOException {
+		this.main = main;
+		this.log = log;
+		this.state = state;
+		ProcessBuilder p = new ProcessBuilder();
+		initFactoryArgs(p.command());
+		p.redirectErrorStream(true);
+		p.directory(new File(getProperty("javax.servlet.context.tempdir",
+				getProperty("java.io.tmpdir"))));
+
+		// Spawn the subprocess
+		log.info("about to create subprocess: " + p.command());
+		log.info("subprocess directory: " + p.directory());
+		process = launchSubprocess(p);
+		channel = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
+				process.getOutputStream())), true);
+		// Log the responses
+		out = new StreamLogger("ForkedStdout", process.getInputStream()) {
+			@Override
+			protected void write(String msg) {
+				log.info(msg);
+			}
+		};
+		err = new StreamLogger("ForkedStderr", process.getErrorStream()) {
+			@Override
+			protected void write(String msg) {
+				log.info(msg);
+			}
+		};
+	}
+
+	@Override
+	public void close() throws IOException, InterruptedException {
+		try {
+			if (process != null) {
+				log.info("about to close down subprocess");
+				channel.close();
+				int code = -1;
+				try {
+					try {
+						code = process.exitValue();
+						log.info("secure-fork process already dead?");
+					} catch (IllegalThreadStateException e) {
+						try {
+							code = process.waitFor();
+						} catch (InterruptedException e1) {
+							log.info("interrupted waiting for natural death of secure-fork process?!");
+							process.destroy();
+							code = process.waitFor();
+						}
+					}
+				} finally {
+					lastExitCode = code;
+					if (code > 128) {
+						log.info("secure-fork process died with signal="
+								+ (code - 128));
+					} else if (code >= 0) {
+						log.info("secure-fork process killed: code=" + code);
+					} else {
+						log.warn("secure-fork process not yet dead");
+					}
+				}
+			}
+		} finally {
+			process = null;
+			channel = null;
+			out.stop();
+			err.stop();
+		}
+	}
+
+	protected void make(String username, String fpn) {
+		log.info("about to request subprocess creation for " + username
+				+ " producing ID " + fpn);
+		channel.println(username + " " + fpn);
+	}
+
+	@Override
+	public RemoteRunFactory make(String username) throws Exception {
+		try {
+			main.getTheRegistry().list(); // Validate registry connection first
+		} catch (ConnectException | ConnectIOException e) {
+			log.warn("connection problems with registry", e);
+		} catch (RemoteException e) {
+			if (e.getCause() != null && e.getCause() instanceof Exception) {
+				throw (Exception) e.getCause();
+			}
+			log.warn("connection problems with registry", e);
+		}
+
+		String fpn = state.getFactoryProcessNamePrefix() + randomUUID();
+		make(username, fpn);
+
+		// Wait for the subprocess to register itself in the RMI registry
+		Calendar deadline = Calendar.getInstance();
+		deadline.add(SECOND, state.getWaitSeconds());
+		Exception lastException = null;
+		lastStartupCheckCount = 0;
+		while (deadline.after(Calendar.getInstance())) {
+			try {
+				sleep(state.getSleepMS());
+				lastStartupCheckCount++;
+				log.info("about to look up resource called " + fpn);
+				RemoteRunFactory f = (RemoteRunFactory) main.getTheRegistry()
+						.lookup(fpn);
+				log.info("successfully connected to factory subprocess " + fpn);
+				main.initInteractionDetails(f);
+				main.registerFactory(username, fpn, f);
+				return f;
+			} catch (InterruptedException ie) {
+				continue;
+			} catch (NotBoundException nbe) {
+				lastException = nbe;
+				log.info("resource \"" + fpn + "\" not yet registered...");
+				continue;
+			} catch (RemoteException re) {
+				// Unpack a remote exception if we can
+				lastException = re;
+				try {
+					if (re.getCause() != null)
+						lastException = (Exception) re.getCause();
+				} catch (Throwable t) {
+					// Ignore!
+				}
+			} catch (Exception e) {
+				lastException = e;
+			}
+		}
+		if (lastException == null)
+			lastException = new InterruptedException();
+		throw lastException;
+	}
+
+	@Override
+	public Integer lastExitCode() {
+		return lastExitCode;
+	}
+
+	@Override
+	public int lastStartupCheckCount() {
+		return lastStartupCheckCount;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/LocalWorkerFactory.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/LocalWorkerFactory.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/LocalWorkerFactory.java
new file mode 100644
index 0000000..803c201
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/localworker/LocalWorkerFactory.java
@@ -0,0 +1,44 @@
+/*
+ */
+package org.taverna.server.master.localworker;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Provider of the configuration of the "localworker.factory" bean, which is
+ * sufficiently complex to be too hard to manufacture directly from the XML
+ * configuration.
+ * 
+ * @author Donal Fellows
+ */
+@Configuration
+public class LocalWorkerFactory {
+	@Bean(name = "localworker.factory")
+	AbstractRemoteRunFactory getLocalworkerFactory(
+			@Value("${backEndFactory}") String mode) throws Exception {
+		if (mode == null || mode.isEmpty() || mode.startsWith("${"))
+			throw new Exception("no value for ${backEndFactory}");
+		Class<?> c = Class.forName(mode);
+		if (AbstractRemoteRunFactory.class.isAssignableFrom(c))
+			return (AbstractRemoteRunFactory) c.newInstance();
+		throw new Exception("unknown remote run factory: " + mode);
+	}
+}