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


Reply via email to