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/06/29 10:55:01 UTC

[14/27] incubator-taverna-plugin-component git commit: package rename folders

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/BaseProfileLocator.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/BaseProfileLocator.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/BaseProfileLocator.java
new file mode 100644
index 0000000..989f411
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/BaseProfileLocator.java
@@ -0,0 +1,154 @@
+package io.github.taverna_extras.component.profile;
+/*
+ * 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.util.Locale.UK;
+import static org.apache.commons.httpclient.HttpStatus.SC_OK;
+import static org.apache.commons.io.FileUtils.writeStringToFile;
+import static org.apache.log4j.Logger.getLogger;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.params.HttpClientParams;
+import org.apache.log4j.Logger;
+import io.github.taverna_extras.component.api.ComponentException;
+import org.apache.taverna.configuration.app.ApplicationConfiguration;
+
+public class BaseProfileLocator {
+	private static final String BASE_PROFILE_PATH = "BaseProfile.xml";
+	private static final String BASE_PROFILE_URI = "http://build.mygrid.org.uk/taverna/BaseProfile.xml";
+	private static final int TIMEOUT = 5000;
+	private static final String pattern = "EEE, dd MMM yyyy HH:mm:ss z";
+	private static final SimpleDateFormat format = new SimpleDateFormat(
+			pattern, UK);
+
+	private Logger logger = getLogger(BaseProfileLocator.class);
+	private ApplicationConfiguration appConfig;
+	private ComponentProfileImpl profile;
+
+	private void locateBaseProfile() {
+		File baseProfileFile = getBaseProfileFile();
+		@SuppressWarnings("unused")
+		boolean load = false;
+		Long remoteBaseProfileTime = null;
+		long localBaseProfileTime = -1;
+
+		HttpClientParams params = new HttpClientParams();
+		params.setConnectionManagerTimeout(TIMEOUT);
+		params.setSoTimeout(TIMEOUT);
+		HttpClient client = new HttpClient(params);
+
+		try {
+			remoteBaseProfileTime = getRemoteBaseProfileTimestamp(client);
+			logger.info("NoticeTime is " + remoteBaseProfileTime);
+		} catch (URISyntaxException e) {
+			logger.error("URI problem", e);
+		} catch (IOException e) {
+			logger.info("Could not read base profile", e);
+		} catch (ParseException e) {
+			logger.error("Could not parse last-modified time", e);
+		}
+		if (baseProfileFile.exists())
+			localBaseProfileTime = baseProfileFile.lastModified();
+
+		try {
+			if ((remoteBaseProfileTime != null)
+					&& (remoteBaseProfileTime > localBaseProfileTime)) {
+				profile = new ComponentProfileImpl(null, new URL(BASE_PROFILE_URI),
+						null);
+				writeStringToFile(baseProfileFile, profile.getXML());
+			}
+		} catch (MalformedURLException e) {
+			logger.error("URI problem", e);
+			profile = null;
+		} catch (ComponentException e) {
+			logger.error("Component Registry problem", e);
+			profile = null;
+		} catch (IOException e) {
+			logger.error("Unable to write profile", e);
+			profile = null;
+		}
+
+		try {
+			if ((profile == null) && baseProfileFile.exists())
+				profile = new ComponentProfileImpl(null, baseProfileFile.toURI()
+						.toURL(), null);
+		} catch (Exception e) {
+			logger.error("URI problem", e);
+			profile = null;
+		}
+	}
+
+	private long parseTime(String timestamp) throws ParseException {
+		timestamp = timestamp.trim();
+		if (timestamp.endsWith(" GMT"))
+			timestamp = timestamp.substring(0, timestamp.length() - 3)
+					+ " +0000";
+		else if (timestamp.endsWith(" BST"))
+			timestamp = timestamp.substring(0, timestamp.length() - 3)
+					+ " +0100";
+		return format.parse(timestamp).getTime();
+	}
+
+	private long getRemoteBaseProfileTimestamp(HttpClient client)
+			throws URISyntaxException, IOException, HttpException,
+			ParseException {
+		URI baseProfileURI = new URI(BASE_PROFILE_URI);
+		HttpMethod method = new GetMethod(baseProfileURI.toString());
+		int statusCode = client.executeMethod(method);
+		if (statusCode != SC_OK) {
+			logger.warn("HTTP status " + statusCode + " while getting "
+					+ baseProfileURI);
+			return -1;
+		}
+		Header h = method.getResponseHeader("Last-Modified");
+		if (h == null)
+			return -1;
+		return parseTime(h.getValue());
+	}
+
+	private File getBaseProfileFile() {
+		File config = new File(appConfig.getApplicationHomeDir().toFile(), "conf");
+		if (!config.exists())
+			config.mkdir();
+		return new File(config, BASE_PROFILE_PATH);
+	}
+
+	public synchronized ComponentProfileImpl getProfile() {
+		if (profile == null)
+			locateBaseProfile();
+		return profile;
+	}
+
+	public void setAppConfig(ApplicationConfiguration appConfig) {
+		this.appConfig = appConfig;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/ComponentProfileImpl.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/ComponentProfileImpl.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/ComponentProfileImpl.java
new file mode 100644
index 0000000..6c7a5b0
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/ComponentProfileImpl.java
@@ -0,0 +1,683 @@
+package io.github.taverna_extras.component.profile;
+/*
+ * 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 org.apache.jena.rdf.model.ModelFactory.createOntologyModel;
+import static java.lang.System.identityHashCode;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
+import static org.apache.log4j.Logger.getLogger;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.transform.stream.StreamSource;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.log4j.Logger;
+import io.github.taverna_extras.component.api.ComponentException;
+import io.github.taverna_extras.component.api.Registry;
+import io.github.taverna_extras.component.api.profile.ActivityProfile;
+import io.github.taverna_extras.component.api.profile.ExceptionHandling;
+import io.github.taverna_extras.component.api.profile.PortProfile;
+import io.github.taverna_extras.component.api.profile.SemanticAnnotationProfile;
+
+import io.github.taverna_extras.component.api.profile.doc.Activity;
+import io.github.taverna_extras.component.api.profile.doc.Ontology;
+import io.github.taverna_extras.component.api.profile.doc.Port;
+import io.github.taverna_extras.component.api.profile.doc.Profile;
+import io.github.taverna_extras.component.api.profile.doc.SemanticAnnotation;
+
+import org.apache.jena.ontology.OntClass;
+import org.apache.jena.ontology.OntModel;
+import org.apache.jena.ontology.OntProperty;
+import static org.apache.taverna.workflowmodel.health.HealthCheck.NO_PROBLEM;
+import org.apache.taverna.workflowmodel.health.RemoteHealthChecker;
+
+/**
+ * A ComponentProfile specifies the inputs, outputs and semantic annotations
+ * that a Component must contain.
+ * 
+ * @author David Withers
+ */
+public class ComponentProfileImpl implements
+		io.github.taverna_extras.component.api.profile.Profile {
+	private static final Logger logger = getLogger(ComponentProfileImpl.class);
+	private static final Map<String, OntModel> ontologyModels = new HashMap<>();
+	private static final JAXBContext jaxbContext;
+	private BaseProfileLocator base;
+	static {
+		try {
+			jaxbContext = JAXBContext.newInstance(Profile.class);
+		} catch (JAXBException e) {
+			// Should never happen! Represents a critical error
+			throw new Error(
+					"Failed to initialize profile deserialization engine", e);
+		}
+	}
+	private io.github.taverna_extras.component.api.profile.Profile parent;
+	private Profile profileDoc;
+	private ExceptionHandling exceptionHandling;
+	private Registry parentRegistry = null;
+	private final Object lock = new Object();
+	private Exception loaderException = null;
+	protected volatile boolean loaded = false;
+
+	public ComponentProfileImpl(URL profileURL, BaseProfileLocator base)
+			throws ComponentException {
+		this(null, profileURL, base);
+	}
+
+	public ComponentProfileImpl(String profileString, BaseProfileLocator base)
+			throws ComponentException {
+		this(null, profileString, base);
+	}
+
+	public ComponentProfileImpl(Registry registry, URI profileURI,
+			BaseProfileLocator base) throws ComponentException,
+			MalformedURLException {
+		this(registry, profileURI.toURL(), base);
+	}
+
+	public ComponentProfileImpl(Registry registry, URL profileURL,
+			BaseProfileLocator base) throws ComponentException {
+		logger.info("Loading profile in " + identityHashCode(this) + " from "
+				+ profileURL);
+		this.base = base;
+		try {
+			URL url = profileURL;
+			if (url.getProtocol().startsWith("http"))
+				url = new URI(url.getProtocol(), url.getAuthority(),
+						url.getPath(), url.getQuery(), url.getRef()).toURL();
+			loadProfile(this, url, base);
+		} catch (MalformedURLException e) {
+			logger.warn("Malformed URL? " + profileURL);
+		} catch (URISyntaxException e) {
+			logger.warn("Malformed URL? " + profileURL);
+		}
+		parentRegistry = registry;
+	}
+
+	public ComponentProfileImpl(Registry registry, String profileString,
+			BaseProfileLocator base) throws ComponentException {
+		logger.info("Loading profile in " + identityHashCode(this)
+				+ " from string");
+		this.base = base;
+		loadProfile(this, profileString, base);
+		this.parentRegistry = registry;
+	}
+
+	private static void loadProfile(final ComponentProfileImpl profile,
+			final Object source, BaseProfileLocator base) {
+		Runnable r = new Runnable() {
+			@Override
+			public void run() {
+				Date start = new Date();
+				if (source instanceof URL)
+					loadProfileFromURL(profile, (URL) source);
+				else if (source instanceof String)
+					loadProfileFromString(profile, (String) source);
+				else
+					throw new IllegalArgumentException(
+							"Bad type of profile source: " + source.getClass());
+				Date end = new Date();
+				logger.info("Loaded profile in " + identityHashCode(profile)
+						+ " (in " + (end.getTime() - start.getTime())
+						+ " msec)");
+			}
+		};
+		if (base.getProfile() == null)
+			// Must load the base profile synchronously, to avoid deadlock
+			r.run();
+		else
+			new Thread(r).start();
+	}
+
+	private static void loadProfileFromURL(ComponentProfileImpl profile, URL source) {
+		try {
+			URLConnection conn = source.openConnection();
+			try {
+				conn.addRequestProperty("Accept", "application/xml,*/*;q=0.1");
+			} catch (Exception e) {
+			}
+			try (InputStream is = conn.getInputStream()) {
+				profile.profileDoc = jaxbContext.createUnmarshaller()
+						.unmarshal(new StreamSource(is), Profile.class)
+						.getValue();
+			}
+		} catch (FileNotFoundException e) {
+			profile.loaderException = e;
+			logger.warn("URL not readable: " + source);
+		} catch (Exception e) {
+			profile.loaderException = e;
+			logger.warn("Failed to load profile.", e);
+		}
+		synchronized (profile.lock) {
+			profile.loaded = true;
+			profile.lock.notifyAll();
+		}
+	}
+
+	private static void loadProfileFromString(ComponentProfileImpl profile,
+			String source) {
+		try {
+			profile.profileDoc = jaxbContext
+					.createUnmarshaller()
+					.unmarshal(new StreamSource(new StringReader(source)),
+							Profile.class).getValue();
+		} catch (Exception e) {
+			profile.loaderException = e;
+			logger.warn("Failed to load profile.", e);
+		}
+		synchronized (profile.lock) {
+			profile.loaded = true;
+			profile.lock.notifyAll();
+		}
+	}
+
+	@Override
+	public Registry getComponentRegistry() {
+		return parentRegistry;
+	}
+
+	@Override
+	public String getXML() throws ComponentException {
+		try {
+			StringWriter stringWriter = new StringWriter();
+			jaxbContext.createMarshaller().marshal(getProfileDocument(),
+					stringWriter);
+			return stringWriter.toString();
+		} catch (JAXBException e) {
+			throw new ComponentException("Unable to serialize profile.", e);
+		}
+	}
+
+	@Override
+	public Profile getProfileDocument() throws ComponentException {
+		try {
+			synchronized (lock) {
+				while (!loaded)
+					lock.wait();
+				if (loaderException != null) {
+					if (loaderException instanceof FileNotFoundException)
+						throw new ComponentException(
+								"Profile not found/readable: "
+										+ loaderException.getMessage(),
+								loaderException);
+					throw new ComponentException(
+							"Problem loading profile definition: "
+									+ loaderException.getMessage(),
+							loaderException);
+				}
+				return profileDoc;
+			}
+		} catch (InterruptedException e) {
+			logger.info("Interrupted during wait for lock.", e);
+			return null;
+		}
+	}
+
+	@Override
+	public String getId() {
+		try {
+			return getProfileDocument().getId();
+		} catch (ComponentException e) {
+			return null;
+		}
+	}
+
+	@Override
+	public String getName() {
+		try {
+			return getProfileDocument().getName();
+		} catch (ComponentException e) {
+			return null;
+		}
+	}
+
+	@Override
+	public String getDescription() {
+		try {
+			return getProfileDocument().getDescription();
+		} catch (ComponentException e) {
+			return null;
+		}
+	}
+
+	/**
+	 * @return Is this the base profile?
+	 */
+	private boolean isBase() {
+		if (base == null)
+			return true;
+		Object o = base.getProfile();
+		return o == null || o == this;
+	}
+
+	private synchronized io.github.taverna_extras.component.api.profile.Profile parent()
+			throws ComponentException {
+		if (parent == null) {
+			try {
+				if (!isBase() && getProfileDocument().getExtends() != null
+						&& parentRegistry != null) {
+					parent = parentRegistry
+							.getComponentProfile(getProfileDocument()
+									.getExtends().getProfileId());
+					if (parent != null)
+						return parent;
+				}
+			} catch (ComponentException e) {
+			}
+			parent = new EmptyProfile();
+		}
+		return parent;
+	}
+
+	@Override
+	public String getOntologyLocation(String ontologyId) {
+		String ontologyURI = null;
+		try {
+			for (Ontology ontology : getProfileDocument().getOntology())
+				if (ontology.getId().equals(ontologyId))
+					ontologyURI = ontology.getValue();
+		} catch (ComponentException e) {
+		}
+		if ((ontologyURI == null) && !isBase())
+			ontologyURI = base.getProfile().getOntologyLocation(ontologyId);
+		return ontologyURI;
+	}
+
+	private Map<String, String> internalGetPrefixMap()
+			throws ComponentException {
+		Map<String, String> result = new TreeMap<>();
+		try {
+			for (Ontology ontology : getProfileDocument().getOntology())
+				result.put(ontology.getId(), ontology.getValue());
+		} catch (ComponentException e) {
+		}
+		result.putAll(parent().getPrefixMap());
+		return result;
+	}
+
+	@Override
+	public Map<String, String> getPrefixMap() throws ComponentException {
+		Map<String, String> result = internalGetPrefixMap();
+		if (!isBase())
+			result.putAll(base.getProfile().getPrefixMap());
+		return result;
+	}
+
+	private OntModel readOntologyFromURI(String ontologyId, String ontologyURI) {
+		logger.info("Reading ontology for " + ontologyId + " from "
+				+ ontologyURI);
+		OntModel model = createOntologyModel();
+		try {
+			URL url = new URL(ontologyURI);
+			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+			// CRITICAL: must be retrieved as correct content type
+			conn.addRequestProperty("Accept",
+					"application/rdf+xml,application/xml;q=0.9");
+			try (InputStream in = conn.getInputStream()) {
+				// TODO Consider whether the encoding is handled right
+				// ontologyModel.read(in, url.toString());
+				model.read(new StringReader(IOUtils.toString(in, "UTF-8")),
+						url.toString());
+			}
+		} catch (MalformedURLException e) {
+			logger.error("Problem reading ontology " + ontologyId, e);
+			return null;
+		} catch (IOException e) {
+			logger.error("Problem reading ontology " + ontologyId, e);
+			return null;
+		} catch (NullPointerException e) {
+			// TODO Why is this different?
+			logger.error("Problem reading ontology " + ontologyId, e);
+			model = createOntologyModel();
+		}
+		return model;
+	}
+
+	private boolean isAccessible(String ontologyURI) {
+		return RemoteHealthChecker.contactEndpoint(null, ontologyURI).getResultId() == NO_PROBLEM;
+	}
+
+	@Override
+	public OntModel getOntology(String ontologyId) {
+		String ontologyURI = getOntologyLocation(ontologyId);
+		synchronized (ontologyModels) {
+			if (ontologyModels.containsKey(ontologyURI))
+				return ontologyModels.get(ontologyURI);
+		}
+
+		// Drop out of critical section while we do I/O
+		if (!isAccessible(ontologyURI)) {
+			logger.warn("Catastrophic problem contacting ontology source.");
+			// Catastrophic problem?!
+			synchronized (ontologyModels) {
+				ontologyModels.put(ontologyURI, null);
+			}
+			return null;
+		}
+		OntModel model = readOntologyFromURI(ontologyId, ontologyURI);
+
+		synchronized (ontologyModels) {
+			if (model != null && !ontologyModels.containsKey(ontologyURI)) {
+				ontologyModels.put(ontologyURI, model);
+			}
+			return ontologyModels.get(ontologyURI);
+		}
+	}
+
+	@Override
+	public List<PortProfile> getInputPortProfiles() {
+		List<PortProfile> portProfiles = new ArrayList<>();
+		try {
+			for (Port port : getProfileDocument().getComponent().getInputPort())
+				portProfiles.add(new PortProfileImpl(this, port));
+		} catch (ComponentException e) {
+		}
+		if (!isBase())
+			portProfiles.addAll(base.getProfile().getInputPortProfiles());
+		return portProfiles;
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getInputSemanticAnnotationProfiles()
+			throws ComponentException {
+		List<SemanticAnnotationProfile> saProfiles = new ArrayList<>();
+		List<PortProfile> portProfiles = getInputPortProfiles();
+		portProfiles.addAll(parent().getInputPortProfiles());
+		for (PortProfile portProfile : portProfiles)
+			saProfiles.addAll(portProfile.getSemanticAnnotations());
+		if (!isBase())
+			saProfiles.addAll(base.getProfile()
+					.getInputSemanticAnnotationProfiles());
+		return getUniqueSemanticAnnotationProfiles(saProfiles);
+	}
+
+	@Override
+	public List<PortProfile> getOutputPortProfiles() {
+		List<PortProfile> portProfiles = new ArrayList<>();
+		try {
+			for (Port port : getProfileDocument().getComponent()
+					.getOutputPort())
+				portProfiles.add(new PortProfileImpl(this, port));
+		} catch (ComponentException e) {
+		}
+		if (!isBase())
+			portProfiles.addAll(base.getProfile().getOutputPortProfiles());
+		return portProfiles;
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getOutputSemanticAnnotationProfiles()
+			throws ComponentException {
+		List<SemanticAnnotationProfile> saProfiles = new ArrayList<>();
+		List<PortProfile> portProfiles = getOutputPortProfiles();
+		portProfiles.addAll(parent().getOutputPortProfiles());
+		for (PortProfile portProfile : portProfiles)
+			saProfiles.addAll(portProfile.getSemanticAnnotations());
+		if (!isBase())
+			saProfiles.addAll(base.getProfile()
+					.getOutputSemanticAnnotationProfiles());
+		return getUniqueSemanticAnnotationProfiles(saProfiles);
+	}
+
+	@Override
+	public List<io.github.taverna_extras.component.api.profile.ActivityProfile> getActivityProfiles() {
+		List<io.github.taverna_extras.component.api.profile.ActivityProfile> activityProfiles = new ArrayList<>();
+		try {
+			for (Activity activity : getProfileDocument().getComponent()
+					.getActivity())
+				activityProfiles.add(new ActivityProfileImpl(this, activity));
+		} catch (ComponentException e) {
+		}
+		return activityProfiles;
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getActivitySemanticAnnotationProfiles()
+			throws ComponentException {
+		List<SemanticAnnotationProfile> saProfiles = new ArrayList<>();
+		List<ActivityProfile> activityProfiles = getActivityProfiles();
+		activityProfiles.addAll(parent().getActivityProfiles());
+		for (ActivityProfile activityProfile : activityProfiles)
+			saProfiles.addAll(activityProfile.getSemanticAnnotations());
+		if (!isBase())
+			saProfiles.addAll(base.getProfile()
+					.getActivitySemanticAnnotationProfiles());
+		return getUniqueSemanticAnnotationProfiles(saProfiles);
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getSemanticAnnotations()
+			throws ComponentException {
+		List<SemanticAnnotationProfile> saProfiles = getComponentProfiles();
+		saProfiles.addAll(parent().getSemanticAnnotations());
+		if (!isBase())
+			saProfiles.addAll(base.getProfile().getSemanticAnnotations());
+		return saProfiles;
+	}
+
+	private List<SemanticAnnotationProfile> getComponentProfiles() {
+		List<SemanticAnnotationProfile> saProfiles = new ArrayList<>();
+		try {
+			for (SemanticAnnotation semanticAnnotation : getProfileDocument()
+					.getComponent().getSemanticAnnotation())
+				saProfiles.add(new SemanticAnnotationProfileImpl(this,
+						semanticAnnotation));
+		} catch (ComponentException e) {
+		}
+		return saProfiles;
+	}
+
+	private List<SemanticAnnotationProfile> getUniqueSemanticAnnotationProfiles(
+			List<SemanticAnnotationProfile> semanticAnnotationProfiles) {
+		List<SemanticAnnotationProfile> uniqueSemanticAnnotations = new ArrayList<>();
+		Set<OntProperty> predicates = new HashSet<>();
+		for (SemanticAnnotationProfile semanticAnnotationProfile : semanticAnnotationProfiles) {
+			OntProperty prop = semanticAnnotationProfile.getPredicate();
+			if (prop != null && !predicates.contains(prop)) {
+				predicates.add(prop);
+				uniqueSemanticAnnotations.add(semanticAnnotationProfile);
+			}
+		}
+		return uniqueSemanticAnnotations;
+	}
+
+	@Override
+	public ExceptionHandling getExceptionHandling() {
+		try {
+			if (exceptionHandling == null
+					&& getProfileDocument().getComponent()
+							.getExceptionHandling() != null)
+				exceptionHandling = new ExceptionHandling(getProfileDocument()
+						.getComponent().getExceptionHandling());
+		} catch (ComponentException e) {
+		}
+		return exceptionHandling;
+	}
+
+	@Override
+	public String toString() {
+		return "ComponentProfile" + "\n  Name : " + getName()
+				+ "\n  Description : " + getDescription()
+				+ "\n  InputPortProfiles : " + getInputPortProfiles()
+				+ "\n  OutputPortProfiles : " + getOutputPortProfiles();
+	}
+
+	@Override
+	public int hashCode() {
+		return 31 + ((getId() == null) ? 0 : getId().hashCode());
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		ComponentProfileImpl other = (ComponentProfileImpl) obj;
+		if (!loaded || !other.loaded)
+			return false;
+		if (getId() == null)
+			return other.getId() == null;
+		return getId().equals(other.getId());
+	}
+
+	public OntClass getClass(String className) {
+		try {
+			for (Ontology ontology : getProfileDocument().getOntology()) {
+				OntModel ontModel = getOntology(ontology.getId());
+				if (ontModel != null) {
+					OntClass result = ontModel.getOntClass(className);
+					if (result != null)
+						return result;
+				}
+			}
+		} catch (ComponentException e) {
+		}
+		return null;
+	}
+
+	@Override
+	public void delete() throws ComponentException {
+		throw new ComponentException("Deletion not supported.");
+	}
+}
+
+/**
+ * A simple do-nothing implementation of a profile. Used when there's no other
+ * option for what a <i>real</i> profile extends.
+ * 
+ * @author Donal Fellows
+ */
+final class EmptyProfile implements
+		io.github.taverna_extras.component.api.profile.Profile {
+	@Override
+	public String getName() {
+		return "";
+	}
+
+	@Override
+	public String getDescription() {
+		return "";
+	}
+
+	@Override
+	public Registry getComponentRegistry() {
+		return null;
+	}
+
+	@Override
+	public String getXML() throws ComponentException {
+		throw new ComponentException("No document.");
+	}
+
+	@Override
+	public Profile getProfileDocument() {
+		return new Profile();
+	}
+
+	@Override
+	public String getId() {
+		return "";
+	}
+
+	@Override
+	public String getOntologyLocation(String ontologyId) {
+		return "";
+	}
+
+	@Override
+	public Map<String, String> getPrefixMap() {
+		return emptyMap();
+	}
+
+	@Override
+	public OntModel getOntology(String ontologyId) {
+		return null;
+	}
+
+	@Override
+	public List<PortProfile> getInputPortProfiles() {
+		return emptyList();
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getInputSemanticAnnotationProfiles() {
+		return emptyList();
+	}
+
+	@Override
+	public List<PortProfile> getOutputPortProfiles() {
+		return emptyList();
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getOutputSemanticAnnotationProfiles() {
+		return emptyList();
+	}
+
+	@Override
+	public List<io.github.taverna_extras.component.api.profile.ActivityProfile> getActivityProfiles() {
+		return emptyList();
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getActivitySemanticAnnotationProfiles() {
+		return emptyList();
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getSemanticAnnotations() {
+		return emptyList();
+	}
+
+	@Override
+	public ExceptionHandling getExceptionHandling() {
+		return null;
+	}
+
+	@Override
+	public void delete() throws ComponentException {
+		throw new ComponentException("Deletion forbidden.");
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/PortProfileImpl.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/PortProfileImpl.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/PortProfileImpl.java
new file mode 100644
index 0000000..53a1190
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/PortProfileImpl.java
@@ -0,0 +1,58 @@
+package io.github.taverna_extras.component.profile;
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import io.github.taverna_extras.component.api.profile.PortProfile;
+import io.github.taverna_extras.component.api.profile.SemanticAnnotationProfile;
+
+import io.github.taverna_extras.component.api.profile.doc.Port;
+import io.github.taverna_extras.component.api.profile.doc.SemanticAnnotation;
+
+/**
+ * Specifies the semantic annotations that a port must have.
+ * 
+ * @author David Withers
+ */
+public class PortProfileImpl implements PortProfile {
+	private final ComponentProfileImpl componentProfile;
+	private final Port port;
+
+	public PortProfileImpl(ComponentProfileImpl componentProfile, Port port) {
+		this.componentProfile = componentProfile;
+		this.port = port;
+	}
+
+	@Override
+	public List<SemanticAnnotationProfile> getSemanticAnnotations() {
+		List<SemanticAnnotationProfile> saProfiles = new ArrayList<>();
+		for (SemanticAnnotation annotation : port.getSemanticAnnotation())
+			saProfiles.add(new SemanticAnnotationProfileImpl(componentProfile,
+					annotation));
+		return saProfiles;
+	}
+
+	@Override
+	public String toString() {
+		return "PortProfile \n  SemanticAnnotations : "
+				+ getSemanticAnnotations();
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/SemanticAnnotationProfileImpl.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/SemanticAnnotationProfileImpl.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/SemanticAnnotationProfileImpl.java
new file mode 100644
index 0000000..a3b5afa
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/profile/SemanticAnnotationProfileImpl.java
@@ -0,0 +1,175 @@
+package io.github.taverna_extras.component.profile;
+/*
+ * 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.io.File.createTempFile;
+import static org.apache.commons.io.FileUtils.writeStringToFile;
+import static org.apache.log4j.Logger.getLogger;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+import io.github.taverna_extras.component.api.profile.SemanticAnnotationProfile;
+
+import io.github.taverna_extras.component.api.profile.doc.SemanticAnnotation;
+
+import org.apache.jena.ontology.Individual;
+import org.apache.jena.ontology.OntClass;
+import org.apache.jena.ontology.OntModel;
+import org.apache.jena.ontology.OntProperty;
+import org.apache.jena.ontology.OntResource;
+
+/**
+ * Definition of a semantic annotation for a component element.
+ * 
+ * @author David Withers
+ */
+public class SemanticAnnotationProfileImpl implements SemanticAnnotationProfile {
+	private static final Logger log = getLogger(SemanticAnnotationProfileImpl.class);
+	private final ComponentProfileImpl componentProfile;
+	private final SemanticAnnotation semanticAnnotation;
+
+	public SemanticAnnotationProfileImpl(ComponentProfileImpl componentProfile,
+			SemanticAnnotation semanticAnnotation) {
+		this.componentProfile = componentProfile;
+		this.semanticAnnotation = semanticAnnotation;
+	}
+
+	/**
+	 * Returns the ontology that defines semantic annotation.
+	 * 
+	 * @return the ontology that defines semantic annotation
+	 */
+	@Override
+	public OntModel getOntology() {
+		String ontology = semanticAnnotation.getOntology();
+		if (ontology == null)
+			return null;
+		return componentProfile.getOntology(ontology);
+	}
+
+	/**
+	 * Returns the predicate for the semantic annotation.
+	 * 
+	 * @return the predicate for the semantic annotation
+	 */
+	@Override
+	public OntProperty getPredicate() {
+		OntModel ontology = getOntology();
+		if (ontology == null)
+			return null;
+		String predicate = semanticAnnotation.getPredicate();
+		if (predicate == null)
+			return null;
+		if (predicate.contains("foaf")) {
+			StringWriter sw = new StringWriter();
+			ontology.writeAll(sw, null, "RDF/XML");
+			try {
+				writeStringToFile(createTempFile("foaf", null), sw.toString());
+			} catch (IOException e) {
+				log.info("failed to write foaf ontology to temporary file", e);
+			}
+		}
+
+		return ontology.getOntProperty(predicate);
+	}
+
+	@Override
+	public String getPredicateString() {
+		return semanticAnnotation.getPredicate();
+	}
+
+	@Override
+	public String getClassString() {
+		return semanticAnnotation.getClazz();
+	}
+
+	/**
+	 * Returns the individual that the semantic annotation must use.
+	 * 
+	 * May be null if no explicit individual is required.
+	 * 
+	 * @return the individual that the semantic annotation must use
+	 */
+	@Override
+	public Individual getIndividual() {
+		String individual = semanticAnnotation.getValue();
+		if (individual == null || individual.isEmpty())
+			return null;
+		return getOntology().getIndividual(individual);
+	}
+
+	/**
+	 * Returns the individuals in the range of the predicate defined in the
+	 * ontology.
+	 * 
+	 * @return the individuals in the range of the predicate defined in the
+	 *         ontology
+	 */
+	@Override
+	public List<Individual> getIndividuals() {
+		OntModel ontology = getOntology();
+		OntProperty prop = getPredicate();
+		if (ontology == null || prop == null)
+			return new ArrayList<>();
+		OntResource range = prop.getRange();
+		if (range == null)
+			return new ArrayList<>();
+		return ontology.listIndividuals(range).toList();
+	}
+
+	@Override
+	public Integer getMinOccurs() {
+		return semanticAnnotation.getMinOccurs().intValue();
+	}
+
+	@Override
+	public Integer getMaxOccurs() {
+		try {
+			return Integer.valueOf(semanticAnnotation.getMaxOccurs());
+		} catch (NumberFormatException e) {
+			return null;
+		}
+	}
+
+	@Override
+	public String toString() {
+		return "SemanticAnnotation " + "\n Predicate : " + getPredicate()
+				+ "\n Individual : " + getIndividual() + "\n Individuals : "
+				+ getIndividuals();
+	}
+
+	@Override
+	public OntClass getRangeClass() {
+		String clazz = this.getClassString();
+		if (clazz != null)
+			return componentProfile.getClass(clazz);
+
+		OntProperty prop = getPredicate();
+		if (prop == null)
+			return null;
+		OntResource range = prop.getRange();
+		if (range != null && range.isClass())
+			return range.asClass();
+		return null;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ClientVersion.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ClientVersion.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ClientVersion.java
new file mode 100644
index 0000000..c9fda59
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ClientVersion.java
@@ -0,0 +1,51 @@
+package io.github.taverna_extras.component.registry;
+/*
+ * 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.io.InputStream;
+import java.util.Properties;
+
+public class ClientVersion {
+	private static final String DEFAULT_VERSION = "1.1.0";
+	public static final String VERSION;
+
+	private ClientVersion() {
+	}
+
+	static {
+		InputStream is = ClientVersion.class
+				.getResourceAsStream("version.properties");
+		String version = DEFAULT_VERSION;
+		if (is != null)
+			try {
+				Properties p = new Properties();
+				p.load(is);
+				version = p.getProperty("project.version", DEFAULT_VERSION);
+			} catch (IOException e) {
+			} finally {
+				try {
+					is.close();
+				} catch (IOException e) {
+				}
+			}
+		VERSION = version;
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/Component.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/Component.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/Component.java
new file mode 100644
index 0000000..afdf2b8
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/Component.java
@@ -0,0 +1,161 @@
+package io.github.taverna_extras.component.registry;
+/*
+ * 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.util.Collections.synchronizedSortedMap;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import io.github.taverna_extras.component.api.ComponentException;
+import io.github.taverna_extras.component.api.Version;
+
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+/**
+ * A Component is a building block for creating Taverna workflows. Components
+ * and must comply with the ComponentProfile of their ComponentFamily.
+ * 
+ * @author David Withers
+ */
+public abstract class Component implements
+		io.github.taverna_extras.component.api.Component {
+	private String name;
+	private String description;
+	private URL url;
+	/**
+	 * Mapping from version numbers to version implementations.
+	 */
+	protected SortedMap<Integer, Version> versionMap = new TreeMap<>();
+
+	protected Component(URL url) {
+		this.url = url;
+	}
+	
+	protected Component(String url) {
+		try {
+			this.url = new URL(url);
+		} catch (MalformedURLException e) {
+			// nothing
+		}
+	}
+
+	protected Component(File fileDir) {
+		try {
+			this.url = fileDir.toURI().toURL();
+		} catch (MalformedURLException e) {
+			// nothing
+		}
+	}
+
+	@Override
+	public final synchronized String getName() {
+		if (name == null)
+			name = internalGetName();
+		return name;
+	}
+
+	/**
+	 * The real implementation of the name fetching. Caching already handled.
+	 * 
+	 * @return The name of the component.
+	 */
+	protected abstract String internalGetName();
+
+	@Override
+	public final synchronized String getDescription() {
+		if (description == null)
+			description = internalGetDescription();
+		return description;
+	}
+
+	/**
+	 * The real implementation of the description fetching. Caching already
+	 * handled.
+	 * 
+	 * @return The description of the component.
+	 */
+	protected abstract String internalGetDescription();
+
+	@Override
+	public final SortedMap<Integer, Version> getComponentVersionMap() {
+		synchronized (versionMap) {
+			checkComponentVersionMap();
+			return synchronizedSortedMap(versionMap);
+		}
+	}
+
+	private void checkComponentVersionMap() {
+		if (versionMap.isEmpty())
+			populateComponentVersionMap();
+	}
+
+	/**
+	 * Create the contents of the {@link #versionMap} field.
+	 */
+	protected abstract void populateComponentVersionMap();
+
+	@Override
+	public final Version getComponentVersion(Integer version)
+			throws ComponentException {
+		synchronized (versionMap) {
+			checkComponentVersionMap();
+			return versionMap.get(version);
+		}
+	}
+
+	@Override
+	public final Version addVersionBasedOn(WorkflowBundle bundle,
+			String revisionComment) throws ComponentException {
+		Version result = internalAddVersionBasedOn(bundle, revisionComment);
+		synchronized (versionMap) {
+			checkComponentVersionMap();
+			versionMap.put(result.getVersionNumber(), result);
+		}
+		return result;
+	}
+
+	/**
+	 * Manufacture a new version of a component. Does not add to the overall
+	 * version map.
+	 * 
+	 * @param bundle
+	 *            The definition of the component.
+	 * @param revisionComment
+	 *            The description of the version.
+	 * @return The new version of the component.
+	 * @throws RegistryException
+	 */
+	protected abstract Version internalAddVersionBasedOn(WorkflowBundle bundle,
+			String revisionComment) throws ComponentException;
+
+	@Override
+	public final URL getComponentURL() {
+		return url;
+	}
+
+	@Override
+	public void delete() throws ComponentException {
+		getFamily().removeComponent(this);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentFamily.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentFamily.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentFamily.java
new file mode 100644
index 0000000..ce8a782
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentFamily.java
@@ -0,0 +1,161 @@
+package io.github.taverna_extras.component.registry;
+/*
+ * 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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.github.taverna_extras.component.api.Component;
+import io.github.taverna_extras.component.api.ComponentException;
+import io.github.taverna_extras.component.api.Registry;
+import io.github.taverna_extras.component.api.Version;
+import io.github.taverna_extras.component.api.profile.Profile;
+
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+/**
+ * A ComponentFamily is a collection of Components that share the same
+ * ComponentProfile.
+ * 
+ * @author David Withers
+ */
+public abstract class ComponentFamily implements
+		io.github.taverna_extras.component.api.Family {
+	private Registry parentRegistry;
+	private String name;
+	private String description;
+	private Profile componentProfile;
+	private ComponentUtil util;
+
+	protected Map<String, Component> componentCache = new HashMap<>();
+
+	public ComponentFamily(Registry componentRegistry, ComponentUtil util) {
+		this.parentRegistry = componentRegistry;
+		this.util = util;
+	}
+
+	@Override
+	public Registry getComponentRegistry() {
+		return parentRegistry;
+	}
+
+	@Override
+	public final synchronized String getName() {
+		if (name == null) {
+			name = internalGetName();
+		}
+		return name;
+	}
+
+	protected abstract String internalGetName();
+
+	@Override
+	public final synchronized String getDescription() {
+		if (description == null) {
+			description = internalGetDescription();
+		}
+		return description;
+	}
+
+	protected abstract String internalGetDescription();
+
+	@Override
+	public final synchronized Profile getComponentProfile()
+			throws ComponentException {
+		if (componentProfile == null)
+			componentProfile = internalGetComponentProfile();
+		if (componentProfile == null) {
+			Profile baseProfile = util.getBaseProfile();
+			if (baseProfile != null) {
+				return baseProfile;
+			}
+		}
+		return componentProfile;
+	}
+
+	protected abstract Profile internalGetComponentProfile()
+			throws ComponentException;
+
+	@Override
+	public final List<Component> getComponents() throws ComponentException {
+		checkComponentCache();
+		return new ArrayList<>(componentCache.values());
+	}
+
+	private void checkComponentCache() throws ComponentException {
+		synchronized (componentCache) {
+			if (componentCache.isEmpty())
+				populateComponentCache();
+		}
+	}
+
+	protected abstract void populateComponentCache() throws ComponentException;
+
+	@Override
+	public final Component getComponent(String componentName)
+			throws ComponentException {
+		checkComponentCache();
+		return componentCache.get(componentName);
+	}
+
+	@Override
+	public final Version createComponentBasedOn(String componentName,
+			String description, WorkflowBundle bundle) throws ComponentException {
+		if (componentName == null)
+			throw new ComponentException("Component name must not be null");
+		if (bundle == null)
+			throw new ComponentException("workflow must not be null");
+		checkComponentCache();
+		if (componentCache.containsKey(componentName))
+			throw new ComponentException("Component name already used");
+		Version version = internalCreateComponentBasedOn(componentName,
+				description, bundle);
+		synchronized (componentCache) {
+			Component c = version.getComponent();
+			componentCache.put(componentName, c);
+		}
+		return version;
+	}
+
+	protected abstract Version internalCreateComponentBasedOn(
+			String componentName, String description, WorkflowBundle bundle)
+			throws ComponentException;
+
+	@Override
+	public final void removeComponent(Component component)
+			throws ComponentException {
+		if (component != null) {
+			checkComponentCache();
+			synchronized (componentCache) {
+				componentCache.remove(component.getName());
+			}
+			internalRemoveComponent(component);
+		}
+	}
+
+	protected abstract void internalRemoveComponent(Component component)
+			throws ComponentException;
+
+	@Override
+	public void delete() throws ComponentException {
+		getComponentRegistry().removeComponentFamily(this);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentImplementationCache.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentImplementationCache.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentImplementationCache.java
new file mode 100644
index 0000000..91846a3
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentImplementationCache.java
@@ -0,0 +1,75 @@
+package io.github.taverna_extras.component.registry;
+/*
+ * 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.currentTimeMillis;
+import static org.apache.log4j.Logger.getLogger;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.apache.log4j.Logger;
+import io.github.taverna_extras.component.api.ComponentException;
+import io.github.taverna_extras.component.api.Version;
+
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+public class ComponentImplementationCache {
+	private class Entry {
+		WorkflowBundle implementation;
+		long timestamp;
+	}
+	private final long VALIDITY = 15 * 60 * 1000;
+	private final Logger logger = getLogger(ComponentImplementationCache.class);
+	private final Map<Version.ID, Entry> cache = new WeakHashMap<>();
+	private ComponentUtil utils;
+
+	public void setComponentUtil(ComponentUtil utils) {
+		this.utils = utils;
+	}
+
+	public WorkflowBundle getImplementation(Version.ID id) throws ComponentException {
+		long now = currentTimeMillis();
+		synchronized (id) {
+			Entry entry = cache.get(id);
+			if (entry != null && entry.timestamp >= now)
+				return entry.implementation;
+			logger.info("before calculate component version for " + id);
+			Version componentVersion;
+			try {
+				componentVersion = utils.getVersion(id);
+			} catch (RuntimeException e) {
+				if (entry != null)
+					return entry.implementation;
+				throw new ComponentException(e.getMessage(), e);
+			}
+			logger.info("calculated component version for " + id + " as "
+					+ componentVersion.getVersionNumber() + "; retrieving dataflow");
+			WorkflowBundle implementation = componentVersion.getImplementation();
+			//DataflowValidationReport report = implementation.checkValidity();
+			//logger.info("component version " + id + " incomplete:"
+			//		+ report.isWorkflowIncomplete() + " valid:"
+			//		+ report.isValid());
+			entry = new Entry();
+			entry.implementation = implementation;
+			entry.timestamp = now + VALIDITY;
+			return cache.put(id, entry).implementation;
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentRegistry.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentRegistry.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentRegistry.java
new file mode 100644
index 0000000..325da27
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentRegistry.java
@@ -0,0 +1,242 @@
+package io.github.taverna_extras.component.registry;
+/*
+ * 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.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import io.github.taverna_extras.component.api.ComponentException;
+import io.github.taverna_extras.component.api.Family;
+import io.github.taverna_extras.component.api.License;
+import io.github.taverna_extras.component.api.SharingPolicy;
+import io.github.taverna_extras.component.api.Version;
+import io.github.taverna_extras.component.api.profile.Profile;
+
+/**
+ * A ComponentRegistry contains ComponentFamilies and ComponentProfiles.
+ * 
+ * @author David Withers
+ */
+public abstract class ComponentRegistry implements
+		io.github.taverna_extras.component.api.Registry {
+	protected Map<String, Family> familyCache = new HashMap<>();
+	protected List<Profile> profileCache = new ArrayList<>();
+	protected List<SharingPolicy> permissionCache = new ArrayList<>();
+	protected List<License> licenseCache = new ArrayList<>();
+
+	private URL registryBase;
+
+	protected ComponentRegistry(URL registryBase) throws ComponentException {
+		this.registryBase = registryBase;
+	}
+
+	protected ComponentRegistry(File fileDir) throws ComponentException {
+		try {
+			this.registryBase = fileDir.toURI().toURL();
+		} catch (MalformedURLException e) {
+			throw new ComponentException(e);
+		}
+	}
+
+	@Override
+	public final List<Family> getComponentFamilies() throws ComponentException {
+		checkFamilyCache();
+		return new ArrayList<Family>(familyCache.values());
+	}
+
+	private void checkFamilyCache() throws ComponentException {
+		synchronized (familyCache) {
+			if (familyCache.isEmpty())
+				populateFamilyCache();
+		}
+	}
+
+	protected abstract void populateFamilyCache() throws ComponentException;
+
+	@Override
+	public final Family getComponentFamily(String familyName)
+			throws ComponentException {
+		checkFamilyCache();
+		return familyCache.get(familyName);
+	}
+
+	@Override
+	public final Family createComponentFamily(String familyName,
+			Profile componentProfile, String description, License license,
+			SharingPolicy sharingPolicy) throws ComponentException {
+		if (familyName == null)
+			throw new ComponentException(
+					"Component family name must not be null");
+		if (componentProfile == null)
+			throw new ComponentException("Component profile must not be null");
+		if (getComponentFamily(familyName) != null)
+			throw new ComponentException("Component family already exists");
+
+		Family result = internalCreateComponentFamily(familyName,
+				componentProfile, description, license, sharingPolicy);
+		checkFamilyCache();
+		synchronized (familyCache) {
+			familyCache.put(familyName, result);
+		}
+		return result;
+	}
+
+	protected abstract Family internalCreateComponentFamily(String familyName,
+			Profile componentProfile, String description, License license,
+			SharingPolicy sharingPolicy) throws ComponentException;
+
+	@Override
+	public final void removeComponentFamily(Family componentFamily)
+			throws ComponentException {
+		if (componentFamily != null) {
+			checkFamilyCache();
+			synchronized (familyCache) {
+				familyCache.remove(componentFamily.getName());
+			}
+		  internalRemoveComponentFamily(componentFamily);
+		}
+	}
+
+	protected abstract void internalRemoveComponentFamily(Family componentFamily)
+			throws ComponentException;
+
+	@Override
+	public final URL getRegistryBase() {
+		return registryBase;
+	}
+
+	@Override
+	public final String getRegistryBaseString() {
+		String urlString = getRegistryBase().toString();
+		if (urlString.endsWith("/"))
+			urlString = urlString.substring(0, urlString.length() - 1);
+		return urlString;
+	}
+
+	private void checkProfileCache() throws ComponentException {
+		synchronized (profileCache) {
+			if (profileCache.isEmpty())
+				populateProfileCache();
+		}
+	}
+
+	protected abstract void populateProfileCache() throws ComponentException;
+
+	@Override
+	public final List<Profile> getComponentProfiles() throws ComponentException {
+		checkProfileCache();
+		return profileCache;
+	}
+
+	@Override
+	public final Profile getComponentProfile(String id)
+			throws ComponentException {
+		// TODO use a map instead of a *linear search*...
+		for (Profile p : getComponentProfiles())
+			if (p.getId().equals(id))
+				return p;
+		return null;
+	}
+
+	@Override
+	public final Profile addComponentProfile(Profile componentProfile,
+			License license, SharingPolicy sharingPolicy)
+			throws ComponentException {
+		if (componentProfile == null) {
+			throw new ComponentException("componentProfile is null");
+		}
+		Profile result = null;
+		checkProfileCache();
+		for (Profile p : getComponentProfiles())
+			if (p.getId().equals(componentProfile.getId())) {
+				result = p;
+				break;
+			}
+
+		if (result == null) {
+			result = internalAddComponentProfile(componentProfile, license,
+					sharingPolicy);
+			synchronized (profileCache) {
+				profileCache.add(result);
+			}
+		}
+		return result;
+	}
+
+	protected abstract Profile internalAddComponentProfile(
+			Profile componentProfile, License license,
+			SharingPolicy sharingPolicy) throws ComponentException;
+
+	private void checkPermissionCache() {
+		synchronized (permissionCache) {
+			if (permissionCache.isEmpty())
+				populatePermissionCache();
+		}
+	}
+
+	protected abstract void populatePermissionCache();
+
+	@Override
+	public final List<SharingPolicy> getPermissions() throws ComponentException {
+		checkPermissionCache();
+		return permissionCache;
+	}
+
+	private void checkLicenseCache() {
+		synchronized (licenseCache) {
+			if (licenseCache.isEmpty())
+				populateLicenseCache();
+		}
+	}
+
+	protected abstract void populateLicenseCache();
+
+	@Override
+	public final List<License> getLicenses() throws ComponentException {
+		checkLicenseCache();
+		return licenseCache;
+	}
+
+	protected License getLicenseByAbbreviation(String licenseString)
+			throws ComponentException {
+		checkLicenseCache();
+		for (License l : getLicenses())
+			if (l.getAbbreviation().equals(licenseString))
+				return l;
+		return null;
+	}
+
+	@Override
+	public abstract License getPreferredLicense() throws ComponentException;
+
+	@Override
+	public abstract Set<Version.ID> searchForComponents(String prefixString,
+			String text) throws ComponentException;
+
+	@Override
+	public String toString() {
+		String[] names = getClass().getName().split("\\.");
+		return names[names.length-1] + ": " + registryBase;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentUtil.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentUtil.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentUtil.java
new file mode 100644
index 0000000..cc31f05
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentUtil.java
@@ -0,0 +1,130 @@
+package io.github.taverna_extras.component.registry;
+/*
+ * 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.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import io.github.taverna_extras.component.api.Component;
+import io.github.taverna_extras.component.api.ComponentException;
+import io.github.taverna_extras.component.api.ComponentFactory;
+import io.github.taverna_extras.component.api.Family;
+import io.github.taverna_extras.component.api.Registry;
+import io.github.taverna_extras.component.api.Version;
+import io.github.taverna_extras.component.api.profile.Profile;
+import io.github.taverna_extras.component.profile.BaseProfileLocator;
+import io.github.taverna_extras.component.profile.ComponentProfileImpl;
+import io.github.taverna_extras.component.registry.local.LocalComponentRegistryFactory;
+import io.github.taverna_extras.component.registry.standard.NewComponentRegistryFactory;
+import org.springframework.beans.factory.annotation.Required;
+
+/**
+ * @author alanrw
+ * @author dkf
+ */
+public class ComponentUtil implements ComponentFactory {
+	private NewComponentRegistryFactory netLocator;
+	private BaseProfileLocator base;
+	private LocalComponentRegistryFactory fileLocator;
+
+	private final Map<String, Registry> cache = new HashMap<>();
+
+	@Required
+	public void setNetworkLocator(NewComponentRegistryFactory locator) {
+		this.netLocator = locator;
+	}
+
+	@Required
+	public void setFileLocator(LocalComponentRegistryFactory fileLocator) {
+		this.fileLocator = fileLocator;
+	}
+
+	@Required
+	public void setBaseLocator(BaseProfileLocator base) {
+		this.base = base;
+	}
+
+	@Override
+	public Registry getRegistry(URL registryBase) throws ComponentException {
+		Registry registry = cache.get(registryBase.toString());
+		if (registry != null)
+			return registry;
+
+		if (registryBase.getProtocol().startsWith("http")) {
+			if (!netLocator.verifyBase(registryBase))
+				throw new ComponentException(
+						"Unable to establish credentials for " + registryBase);
+			registry = netLocator.getComponentRegistry(registryBase);
+		} else
+			registry = fileLocator.getComponentRegistry(registryBase);
+		cache.put(registryBase.toString(), registry);
+		return registry;
+	}
+
+	@Override
+	public Family getFamily(URL registryBase, String familyName)
+			throws ComponentException {
+		return getRegistry(registryBase).getComponentFamily(familyName);
+	}
+
+	@Override
+	public Component getComponent(URL registryBase, String familyName,
+			String componentName) throws ComponentException {
+		return getRegistry(registryBase).getComponentFamily(familyName)
+				.getComponent(componentName);
+	}
+
+	@Override
+	public Version getVersion(URL registryBase, String familyName,
+			String componentName, Integer componentVersion)
+			throws ComponentException {
+		return getRegistry(registryBase).getComponentFamily(familyName)
+				.getComponent(componentName)
+				.getComponentVersion(componentVersion);
+	}
+
+	@Override
+	public Version getVersion(Version.ID ident) throws ComponentException {
+		return getVersion(ident.getRegistryBase(), ident.getFamilyName(),
+				ident.getComponentName(), ident.getComponentVersion());
+	}
+
+	@Override
+	public Component getComponent(Version.ID ident) throws ComponentException {
+		return getComponent(ident.getRegistryBase(), ident.getFamilyName(),
+				ident.getComponentName());
+	}
+
+	@Override
+	public Profile getProfile(URL url) throws ComponentException {
+		Profile p = new ComponentProfileImpl(url, base);
+		p.getProfileDocument(); // force immediate loading
+		return p;
+	}
+
+	@Override
+	public Profile getBaseProfile() throws ComponentException {
+		return base.getProfile();
+	}
+
+	public BaseProfileLocator getBaseProfileLocator() {
+		return base;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentVersion.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentVersion.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentVersion.java
new file mode 100644
index 0000000..ab95218
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentVersion.java
@@ -0,0 +1,77 @@
+package io.github.taverna_extras.component.registry;
+/*
+ * 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 io.github.taverna_extras.component.api.Component;
+import io.github.taverna_extras.component.api.ComponentException;
+
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+public abstract class ComponentVersion implements
+		io.github.taverna_extras.component.api.Version {
+	private Integer versionNumber;
+	private String description;
+	private Component component;
+
+	protected ComponentVersion(Component component) {
+		this.component = component;
+	}
+
+	@Override
+	public final synchronized Integer getVersionNumber() {
+		if (versionNumber == null)
+			versionNumber = internalGetVersionNumber();
+		return versionNumber;
+	}
+
+	protected abstract Integer internalGetVersionNumber();
+
+	@Override
+	public final synchronized String getDescription() {
+		if (description == null)
+			description = internalGetDescription();
+
+		return description;
+	}
+
+	protected abstract String internalGetDescription();
+
+	@Override
+	public final synchronized WorkflowBundle getImplementation()
+			throws ComponentException {
+		// Cached in dataflow cache
+		return internalGetImplementation();
+	}
+
+	protected abstract WorkflowBundle internalGetImplementation()
+			throws ComponentException;
+
+	@Override
+	public final Component getComponent() {
+		return component;
+	}
+
+	@Override
+	public ID getID() {
+		Component c = getComponent();
+		return new ComponentVersionIdentification(c.getRegistry()
+				.getRegistryBase(), c.getFamily().getName(), c.getName(),
+				getVersionNumber());
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentVersionIdentification.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentVersionIdentification.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentVersionIdentification.java
new file mode 100644
index 0000000..4cecab5
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/ComponentVersionIdentification.java
@@ -0,0 +1,212 @@
+
+package io.github.taverna_extras.component.registry;
+/*
+ * 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.URL;
+
+import io.github.taverna_extras.component.api.Family;
+import io.github.taverna_extras.component.api.Registry;
+import io.github.taverna_extras.component.api.Version;
+import io.github.taverna_extras.component.api.Version.ID;
+
+/**
+ * @author alanrw
+ * 
+ */
+public class ComponentVersionIdentification implements
+		io.github.taverna_extras.component.api.Version.ID {
+	private static final long serialVersionUID = 1768548650702925916L;
+	private URL registryBase;
+	private String familyName;
+	private String componentName;
+	private Integer componentVersion;
+
+	public ComponentVersionIdentification(URL registryBase, String familyName,
+			String componentName, Integer componentVersion) {
+		super();
+		this.registryBase = registryBase;
+		this.familyName = familyName;
+		this.componentName = componentName;
+		this.componentVersion = componentVersion;
+	}
+
+	public ComponentVersionIdentification(Registry registry, Family family,
+			io.github.taverna_extras.component.api.Component component, Integer version) {
+		this(registry.getRegistryBase(), family.getName(), component.getName(), version);
+	}
+
+	public ComponentVersionIdentification(Version.ID toBeCopied) {
+		this.registryBase = toBeCopied.getRegistryBase();
+		this.familyName = toBeCopied.getFamilyName();
+		this.componentName = toBeCopied.getComponentName();
+		this.componentVersion = toBeCopied.getComponentVersion();
+	}
+
+	/**
+	 * @return the registryBase
+	 */
+	@Override
+	public URL getRegistryBase() {
+		return registryBase;
+	}
+
+	/**
+	 * @return the familyName
+	 */
+	@Override
+	public String getFamilyName() {
+		return familyName;
+	}
+
+	/**
+	 * @return the componentName
+	 */
+	@Override
+	public String getComponentName() {
+		return componentName;
+	}
+
+	/**
+	 * @return the componentVersion
+	 */
+	@Override
+	public Integer getComponentVersion() {
+		return componentVersion;
+	}
+
+	/**
+	 * @param componentVersion
+	 *            the componentVersion to set
+	 */
+	public void setComponentVersion(Integer componentVersion) {
+		this.componentVersion = componentVersion;
+	}
+
+	/**
+	 * @param registryBase
+	 *            the registryBase to set
+	 */
+	public void setRegistryBase(URL registryBase) {
+		this.registryBase = registryBase;
+	}
+
+	/**
+	 * @param familyName
+	 *            the familyName to set
+	 */
+	public void setFamilyName(String familyName) {
+		this.familyName = familyName;
+	}
+
+	/**
+	 * @param componentName
+	 *            the componentName to set
+	 */
+	public void setComponentName(String componentName) {
+		this.componentName = componentName;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result
+				+ ((componentName == null) ? 0 : componentName.hashCode());
+		result = prime
+				* result
+				+ ((componentVersion == null) ? 0 : componentVersion.hashCode());
+		result = prime * result
+				+ ((familyName == null) ? 0 : familyName.hashCode());
+		result = prime * result
+				+ ((registryBase == null) ? 0 : registryBase.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		ComponentVersionIdentification other = (ComponentVersionIdentification) obj;
+		if (componentName == null) {
+			if (other.componentName != null)
+				return false;
+		} else if (!componentName.equals(other.componentName))
+			return false;
+		if (componentVersion == null) {
+			if (other.componentVersion != null)
+				return false;
+		} else if (!componentVersion.equals(other.componentVersion))
+			return false;
+		if (familyName == null) {
+			if (other.familyName != null)
+				return false;
+		} else if (!familyName.equals(other.familyName))
+			return false;
+		if (registryBase == null) {
+			if (other.registryBase != null)
+				return false;
+		} else if (!registryBase.toString().equals(other.registryBase.toString()))
+			return false;
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return getComponentName() + " V. " + getComponentVersion()
+				+ " in family " + getFamilyName() + " on "
+				+ getRegistryBase().toExternalForm();
+	}
+
+	@Override
+	public boolean mostlyEqualTo(ID id) {
+		if (this == id)
+			return true;
+		if (id == null)
+			return false;
+		if (getClass() != id.getClass())
+			return false;
+		ComponentVersionIdentification other = (ComponentVersionIdentification) id;
+		if (componentName == null) {
+			if (other.componentName != null)
+				return false;
+		} else if (!componentName.equals(other.componentName))
+			return false;
+		if (familyName == null) {
+			if (other.familyName != null)
+				return false;
+		} else if (!familyName.equals(other.familyName))
+			return false;
+		if (registryBase == null) {
+			if (other.registryBase != null)
+				return false;
+		} else if (!registryBase.toString().equals(other.registryBase.toString()))
+			return false;
+		return true;
+	}
+
+	@Override
+	public boolean mostlyEqualTo(io.github.taverna_extras.component.api.Component c) {
+		return mostlyEqualTo(new ComponentVersionIdentification(c.getRegistry(), c.getFamily(), c, 0));
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/local/LocalComponent.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/local/LocalComponent.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/local/LocalComponent.java
new file mode 100644
index 0000000..697f57f
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/local/LocalComponent.java
@@ -0,0 +1,149 @@
+package io.github.taverna_extras.component.registry.local;
+/*
+ * 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 org.apache.commons.io.FileUtils.readFileToString;
+import static org.apache.commons.io.FileUtils.writeStringToFile;
+import static org.apache.log4j.Logger.getLogger;
+import static io.github.taverna_extras.component.registry.local.LocalComponentRegistry.ENC;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.NoSuchElementException;
+
+import org.apache.log4j.Logger;
+import io.github.taverna_extras.component.api.ComponentException;
+import io.github.taverna_extras.component.api.Family;
+import io.github.taverna_extras.component.api.Registry;
+import io.github.taverna_extras.component.api.Version;
+import io.github.taverna_extras.component.registry.Component;
+import io.github.taverna_extras.component.utils.SystemUtils;
+
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+/**
+ * @author alanrw
+ * 
+ */
+class LocalComponent extends Component {
+	static final String COMPONENT_FILENAME = "dataflow.t2flow";
+	private final File componentDir;
+	private final LocalComponentRegistry registry;
+	private final LocalComponentFamily family;
+	private static Logger logger = getLogger(LocalComponent.class);
+	private SystemUtils system;
+
+	public LocalComponent(File componentDir, LocalComponentRegistry registry,
+			LocalComponentFamily family, SystemUtils system) {
+		super(componentDir);
+		this.system = system;
+		this.componentDir = componentDir;
+		this.registry = registry;
+		this.family = family;
+	}
+
+	@Override
+	protected final Version internalAddVersionBasedOn(WorkflowBundle bundle,
+			String revisionComment) throws ComponentException {
+		Integer nextVersionNumber = 1;
+		try {
+			nextVersionNumber = getComponentVersionMap().lastKey() + 1;
+		} catch (NoSuchElementException e) {
+			// This is OK
+		}
+		File newVersionDir = new File(componentDir,
+				nextVersionNumber.toString());
+		newVersionDir.mkdirs();
+		LocalComponentVersion newComponentVersion = new LocalComponentVersion(
+				this, newVersionDir, system);
+		try {
+			system.saveBundle(bundle, new File(newVersionDir,
+					COMPONENT_FILENAME));
+		} catch (Exception e) {
+			throw new ComponentException("Unable to save component version", e);
+		}
+		File revisionCommentFile = new File(newVersionDir, "description");
+		try {
+			writeStringToFile(revisionCommentFile, revisionComment, ENC);
+		} catch (IOException e) {
+			throw new ComponentException("Could not write out description", e);
+		}
+
+		return newComponentVersion;
+	}
+
+	@Override
+	protected final String internalGetName() {
+		return componentDir.getName();
+	}
+
+	@Override
+	protected final void populateComponentVersionMap() {
+		for (File subFile : componentDir.listFiles())
+			try {
+				if (subFile.isDirectory())
+					versionMap.put(Integer.valueOf(subFile.getName()),
+							new LocalComponentVersion(this, subFile, system));
+			} catch (NumberFormatException e) {
+				// Ignore
+			}
+	}
+
+	@Override
+	public int hashCode() {
+		return 31 + ((componentDir == null) ? 0 : componentDir.hashCode());
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		LocalComponent other = (LocalComponent) obj;
+		if (componentDir == null)
+			return (other.componentDir == null);
+		return componentDir.equals(other.componentDir);
+	}
+
+	@Override
+	protected final String internalGetDescription() {
+		File descriptionFile = new File(componentDir, "description");
+		try {
+			if (descriptionFile.isFile())
+				return readFileToString(descriptionFile);
+		} catch (IOException e) {
+			logger.error("failed to get description from " + descriptionFile, e);
+		}
+		return "";
+	}
+
+	@Override
+	public Registry getRegistry() {
+		return registry;
+	}
+
+	@Override
+	public Family getFamily() {
+		return family;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-plugin-component/blob/b7b61e71/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/local/LocalComponentFamily.java
----------------------------------------------------------------------
diff --git a/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/local/LocalComponentFamily.java b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/local/LocalComponentFamily.java
new file mode 100644
index 0000000..b9a692c
--- /dev/null
+++ b/taverna-component-activity/src/main/java/io/github/taverna_extras/component/registry/local/LocalComponentFamily.java
@@ -0,0 +1,155 @@
+package io.github.taverna_extras.component.registry.local;
+/*
+ * 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 org.apache.commons.io.FileUtils.deleteDirectory;
+import static org.apache.commons.io.FileUtils.readFileToString;
+import static org.apache.commons.io.FileUtils.writeStringToFile;
+import static org.apache.log4j.Logger.getLogger;
+import static io.github.taverna_extras.component.registry.local.LocalComponentRegistry.ENC;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.log4j.Logger;
+import io.github.taverna_extras.component.api.Component;
+import io.github.taverna_extras.component.api.ComponentException;
+import io.github.taverna_extras.component.api.Version;
+import io.github.taverna_extras.component.api.profile.Profile;
+import io.github.taverna_extras.component.registry.ComponentFamily;
+import io.github.taverna_extras.component.registry.ComponentUtil;
+import io.github.taverna_extras.component.utils.SystemUtils;
+
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+/**
+ * @author alanrw
+ * 
+ */
+class LocalComponentFamily extends ComponentFamily {
+	private static Logger logger = getLogger(LocalComponentFamily.class);
+	private static final String PROFILE = "profile";
+
+	private final File componentFamilyDir;
+	private SystemUtils system;
+
+	public LocalComponentFamily(LocalComponentRegistry parentRegistry,
+			File componentFamilyDir, ComponentUtil util, SystemUtils system) {
+		super(parentRegistry, util);
+		this.componentFamilyDir = componentFamilyDir;
+		this.system = system;
+	}
+
+	@Override
+	protected final Profile internalGetComponentProfile()
+			throws ComponentException {
+		LocalComponentRegistry parentRegistry = (LocalComponentRegistry) getComponentRegistry();
+		File profileFile = new File(componentFamilyDir, PROFILE);
+		String profileName;
+		try {
+			profileName = readFileToString(profileFile, ENC);
+		} catch (IOException e) {
+			throw new ComponentException("Unable to read profile name", e);
+		}
+		for (Profile p : parentRegistry.getComponentProfiles())
+			if (p.getName().equals(profileName))
+				return p;
+		return null;
+	}
+
+	@Override
+	protected void populateComponentCache() throws ComponentException {
+		for (File subFile : componentFamilyDir.listFiles()) {
+			if (!subFile.isDirectory())
+				continue;
+			LocalComponent newComponent = new LocalComponent(subFile,
+					(LocalComponentRegistry) getComponentRegistry(), this,
+					system);
+			componentCache.put(newComponent.getName(), newComponent);
+		}
+	}
+
+	@Override
+	protected final String internalGetName() {
+		return componentFamilyDir.getName();
+	}
+
+	@Override
+	protected final Version internalCreateComponentBasedOn(
+			String componentName, String description, WorkflowBundle bundle)
+			throws ComponentException {
+		File newSubFile = new File(componentFamilyDir, componentName);
+		if (newSubFile.exists())
+			throw new ComponentException("Component already exists");
+		newSubFile.mkdirs();
+		File descriptionFile = new File(newSubFile, "description");
+		try {
+			writeStringToFile(descriptionFile, description, ENC);
+		} catch (IOException e) {
+			throw new ComponentException("Could not write out description", e);
+		}
+		LocalComponent newComponent = new LocalComponent(newSubFile,
+				(LocalComponentRegistry) getComponentRegistry(), this, system);
+
+		return newComponent.addVersionBasedOn(bundle, "Initial version");
+	}
+
+	@Override
+	public int hashCode() {
+		return 31 + ((componentFamilyDir == null) ? 0 : componentFamilyDir
+				.hashCode());
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		LocalComponentFamily other = (LocalComponentFamily) obj;
+		if (componentFamilyDir == null)
+			return (other.componentFamilyDir == null);
+		return componentFamilyDir.equals(other.componentFamilyDir);
+	}
+
+	@Override
+	protected final String internalGetDescription() {
+		File descriptionFile = new File(componentFamilyDir, "description");
+		try {
+			if (descriptionFile.isFile())
+				return readFileToString(descriptionFile);
+		} catch (IOException e) {
+			logger.error("failed to get description from " + descriptionFile, e);
+		}
+		return "";
+	}
+
+	@Override
+	protected final void internalRemoveComponent(Component component)
+			throws ComponentException {
+		File componentDir = new File(componentFamilyDir, component.getName());
+		try {
+			deleteDirectory(componentDir);
+		} catch (IOException e) {
+			throw new ComponentException("Unable to delete component", e);
+		}
+	}
+}