Bug 571223 - Add configuration resource

Change-Id: Ic0cab0fb8f898dd47c9305d684a7c85ce0a00cd0
Signed-off-by: Dirk Fauth <Dirk.Fauth@de.bosch.com>
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/CloudServiceExecutionRunnable.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/CloudServiceExecutionRunnable.java
index f5574c2..be89149 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/CloudServiceExecutionRunnable.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/CloudServiceExecutionRunnable.java
@@ -53,8 +53,14 @@
 		try {
 			Path inputFile = this.storageService.load(this.uuid, this.originalFilename);
 			
-			ServiceNodeProcessingTask rootTask = new ServiceNodeProcessingTask(this.workflowStatus.getServiceRootNode(),
-					inputFile, storageService, messagingTemplate, uuid, workflowStatus);
+			ServiceNodeProcessingTask rootTask = 
+					new ServiceNodeProcessingTask(
+							this.workflowStatus.getServiceRootNode(),
+							inputFile,
+							this.storageService,
+							this.messagingTemplate,
+							this.uuid,
+							this.workflowStatus);
 
 			logger.info("started root task");
 			rootTask.fork();
@@ -68,33 +74,34 @@
 					this.workflowStatus, 
 					this.storageService.load(this.uuid, "workflowstatus.json").toFile());
 			
-			// ensure that the done message is sent in any way to ensure the ui gets the final update
-			// needed in case the process finishes while the page reloads
-			long start = System.currentTimeMillis();
-			long end = System.currentTimeMillis();
-			
-			long timeout = 10_000l;
-			
-			while (!this.workflowStatus.isConnected()) {
-				try {
-					Thread.sleep(1000);
-				} catch (InterruptedException e) {
-					// Restore interrupted state...
-				    Thread.currentThread().interrupt();
+			if (this.messagingTemplate != null) {
+				// ensure that the done message is sent in any way to ensure the ui gets the final update
+				// needed in case the process finishes while the page reloads
+				long start = System.currentTimeMillis();
+				long end = System.currentTimeMillis();
+				
+				long timeout = 10_000l;
+				
+				while (!this.workflowStatus.isConnected()) {
+					try {
+						Thread.sleep(1000);
+					} catch (InterruptedException e) {
+						// Restore interrupted state...
+						Thread.currentThread().interrupt();
+					}
+					
+					end = System.currentTimeMillis();
+					
+					// don't wait longer than 10 seconds to get connected
+					if (timeout > 0 && (end - start) > timeout) {
+						break;
+					}
 				}
 				
-				end = System.currentTimeMillis();
-				
-				// don't wait longer than 10 seconds to get connected
-				if (timeout > 0 && (end - start) > timeout) {
-					break;
-				}
+				this.messagingTemplate.convertAndSend(
+						"/topic/process-updates/" + this.uuid, 
+						new ProcessLog(Action.DONE, null, this.uuid));
 			}
-
-			this.messagingTemplate.convertAndSend(
-					"/topic/process-updates/" + this.uuid, 
-					new ProcessLog(Action.DONE, null, this.uuid));
 		}
 	}
-	
-}
+}
\ No newline at end of file
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfiguration.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfiguration.java
index f113c98..de52dff 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfiguration.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfiguration.java
@@ -1,5 +1,5 @@
 /*********************************************************************************
- * Copyright (c) 2020 Robert Bosch GmbH and others.
+ * Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -23,6 +23,10 @@
 	 * Needed for user interface.
 	 */
 	private String serviceName;
+	/**
+	 * The description of the service to show in the user interface, so a user knows what a service is supposed to do.
+	 */
+	private String serviceDescription;
 	private ArrayList<ServiceConfigurationParameter> parameter = new ArrayList<>();
 	
 	public ServiceConfiguration() {
@@ -41,14 +45,20 @@
 		this.serviceName = serviceName;
 	}
 
+	public String getServiceDescription() {
+		return serviceDescription;
+	}
+
+	public void setServiceDescription(String serviceDescription) {
+		this.serviceDescription = serviceDescription;
+	}
+
 	public void addParameter(ServiceConfigurationParameter param) {
 		this.parameter.add(param);
 	}
 	
 	public List<ServiceConfigurationParameter> getParameterList() {
-		ArrayList<ServiceConfigurationParameter> params = new ArrayList<>(this.parameter);
-		params.sort((o1, o2) -> o1.getKey().compareTo(o2.getKey()));
-		return params;
+		return new ArrayList<>(this.parameter);
 	}
 	
 	public ServiceConfigurationParameter getParameter(String key) {
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationDefinition.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationDefinition.java
new file mode 100644
index 0000000..19e4bbe
--- /dev/null
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationDefinition.java
@@ -0,0 +1,140 @@
+/*********************************************************************************
+ * Copyright (c) 2021 Robert Bosch GmbH and others.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Robert Bosch GmbH - initial API and implementation
+ ********************************************************************************
+ */
+package org.eclipse.app4mc.cloud.manager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * The service configuration definition that is provided by a service.
+ */
+@JsonDeserialize(using = ServiceConfigurationDefinitionDeserializer.class)
+public class ServiceConfigurationDefinition {
+
+	/**
+	 * The description of the service to explain what the service does.
+	 */
+	private String description;
+	/**
+	 * The file type that is accepted as input, e.g. amxmi, json, btf.
+	 */
+	private String inputType;
+	/**
+	 * The accepted input version, for the Amalthea Model e.g. 0.9.9 or 1.0.0.
+	 */
+	private String inputVersion;
+	/**
+	 * Whether the service supports input provided as archive.
+	 */
+	private boolean inputArchiveSupported = false;
+	
+	/**
+	 * The file type that is produced as a result, e.g. amxmi, json, btf.
+	 */
+	private String outputType;
+	/**
+	 * The produced output version, for the Amalthea Model e.g. 0.9.9 or 1.0.0.
+	 */
+	private String outputVersion;
+	/**
+	 * Whether the service supports producing the output as archive.
+	 */
+	private boolean outputArchiveSupported = false;
+
+	/**
+	 * The configuration parameter the service supports.
+	 */
+	private ArrayList<ServiceConfigurationParameter> parameter = new ArrayList<>();
+	
+	public ServiceConfigurationDefinition() {
+		// empty constructor needed for JSON serialization 
+	}
+	
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+
+	public String getInputType() {
+		return inputType;
+	}
+
+	public void setInputType(String inputType) {
+		this.inputType = inputType;
+	}
+
+	public String getInputVersion() {
+		return inputVersion;
+	}
+
+	public void setInputVersion(String inputVersion) {
+		this.inputVersion = inputVersion;
+	}
+
+	public boolean isInputArchiveSupported() {
+		return this.inputArchiveSupported;
+	}
+	
+	public void setInputArchiveSupported(boolean supported) {
+		this.inputArchiveSupported = supported;
+	}
+
+	public String getOutputType() {
+		return outputType;
+	}
+
+	public void setOutputType(String outputType) {
+		this.outputType = outputType;
+	}
+
+	public String getOutputVersion() {
+		return outputVersion;
+	}
+
+	public void setOutputVersion(String outputVersion) {
+		this.outputVersion = outputVersion;
+	}
+
+	public boolean isOutputArchiveSupported() {
+		return this.outputArchiveSupported;
+	}
+	
+	public void setOutputArchiveSupported(boolean supported) {
+		this.outputArchiveSupported = supported;
+	}
+	
+	public void addParameter(ServiceConfigurationParameter param) {
+		this.parameter.add(param);
+	}
+	
+	public List<ServiceConfigurationParameter> getParameterList() {
+		ArrayList<ServiceConfigurationParameter> params = new ArrayList<>(this.parameter);
+		params.sort((o1, o2) -> o1.getName().compareTo(o2.getName()));
+		return params;
+	}
+	
+	public ServiceConfigurationParameter getParameter(String key) {
+		for (ServiceConfigurationParameter param : this.parameter) {
+			if (param.getKey().equals(key)) {
+				return param;
+			}
+		}
+		return null;
+	}
+}
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationDefinitionDeserializer.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationDefinitionDeserializer.java
new file mode 100644
index 0000000..491a476
--- /dev/null
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationDefinitionDeserializer.java
@@ -0,0 +1,140 @@
+/*********************************************************************************
+ * Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Robert Bosch GmbH - initial API and implementation
+ ********************************************************************************
+ */
+package org.eclipse.app4mc.cloud.manager;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+/**
+ * Custom {@link StdDeserializer} for {@link ServiceConfigurationDefinition} objects.
+ */
+public class ServiceConfigurationDefinitionDeserializer extends StdDeserializer<ServiceConfigurationDefinition> {
+	
+	private static final long serialVersionUID = 857606567523680918L;
+	
+	protected ServiceConfigurationDefinitionDeserializer() {
+		super(ServiceConfigurationDefinition.class);
+	}
+
+    @Override
+    public ServiceConfigurationDefinition deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+    	ServiceConfigurationDefinition result = new ServiceConfigurationDefinition();
+        JsonNode node = jp.getCodec().readTree(jp);
+        
+        JsonNode description = node.get("description");
+        if (description != null) {
+        	result.setDescription(description.asText());
+        }
+        
+        JsonNode input = node.get("input");
+        if (input != null) {
+        	JsonNode typeNode = input.get("type");
+        	if (typeNode != null) {
+        		result.setInputType(typeNode.asText());
+        	}
+        	JsonNode versionNode = input.get("version");
+        	if (versionNode != null) {
+        		result.setInputVersion(versionNode.asText());
+        	}
+        	JsonNode archiveNode = input.get("archive-supported");
+        	if (archiveNode != null) {
+        		result.setInputArchiveSupported(archiveNode.asBoolean());
+        	}
+        }
+        
+        JsonNode output = node.get("output");
+        if (output != null) {
+        	JsonNode typeNode = output.get("type");
+        	if (typeNode != null) {
+        		result.setOutputType(typeNode.asText());
+        	}
+        	JsonNode versionNode = output.get("version");
+        	if (versionNode != null) {
+        		result.setOutputVersion(versionNode.asText());
+        	}
+        	JsonNode archiveNode = output.get("archive-supported");
+        	if (archiveNode != null) {
+        		result.setOutputArchiveSupported(archiveNode.asBoolean());
+        	}
+        }
+
+        JsonNode parameter = node.get("parameter");
+        if (parameter != null) {
+        	parameter.fields().forEachRemaining(parameterField -> {
+        		deserializeParameterNode(result, parameterField.getKey(), parameterField.getValue());
+        	});
+        }
+        
+    	return result;
+    }
+    
+    private void deserializeParameterNode(
+    		ServiceConfigurationDefinition result, 
+    		String parameterKey, 
+    		JsonNode fields) {
+    	
+    	ServiceConfigurationParameter param = new ServiceConfigurationParameter();
+    	param.setKey(parameterKey);
+    	
+    	JsonNode name = fields.get("name");
+    	if (name != null) {
+    		param.setName(name.asText());
+    	}
+    	
+    	JsonNode description = fields.get("description");
+    	if (description != null) {
+    		param.setDescription(description.asText());
+    	}
+    	
+    	JsonNode type = fields.get("type");
+    	if (type != null) {
+    		param.setType(type.asText());
+    	}
+    	
+    	JsonNode value = fields.get("value");
+    	if (value != null) {
+    		param.setValue(value.asText());
+    	}
+    	
+    	JsonNode cardinality = fields.get("cardinality");
+    	if (cardinality != null) {
+    		param.setCardinality(cardinality.asText());
+    	}
+    	
+    	JsonNode mandatory = fields.get("mandatory");
+    	if (mandatory != null) {
+    		param.setMandatory(mandatory.asBoolean());
+    	}
+    	
+    	JsonNode values = fields.get("values");
+    	if (values != null) {
+    		if (values.isArray()) {
+    			ArrayList<String> valueList = new ArrayList<>();
+    			values.forEach(v -> valueList.add(v.asText()));
+    			param.setPossibleValues(valueList);
+    		} else {
+    			param.setPossibleValues(Arrays.asList(values.asText()));
+    		}
+    	}
+    	
+    	result.addParameter(param);
+    }
+}
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationParameter.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationParameter.java
index 9f21b40..317eddc 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationParameter.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfigurationParameter.java
@@ -1,5 +1,5 @@
 /*********************************************************************************
- * Copyright (c) 2020 Robert Bosch GmbH and others.
+ * Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -19,9 +19,10 @@
 public class ServiceConfigurationParameter {
 
 	private String name;
+	private String description;
 	private String key;
 	private String value;
-	private String type;
+	private String type = "String";
 	private String cardinality = "single";
 	private boolean mandatory = false;
 	private List<String> possibleValues = new ArrayList<>();
@@ -35,6 +36,14 @@
 		this.name = name;
 	}
 
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+
 	public String getKey() {
 		return key;
 	}
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceNodeProcessingTask.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceNodeProcessingTask.java
index 92b9b4b..a6a17b8 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceNodeProcessingTask.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceNodeProcessingTask.java
@@ -51,8 +51,14 @@
 
 	private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
-	public ServiceNodeProcessingTask(ServiceNode n, Path inputFile, StorageService storageService,
-			SimpMessageSendingOperations messagingTemplate, String uuid, WorkflowStatus ws) {
+	public ServiceNodeProcessingTask(
+			ServiceNode n, 
+			Path inputFile, 
+			StorageService storageService,
+			SimpMessageSendingOperations messagingTemplate, 
+			String uuid, 
+			WorkflowStatus ws) {
+		
 		this.storageService = storageService;
 		this.messagingTemplate = messagingTemplate;
 		this.workflowStatus = ws;
@@ -63,41 +69,31 @@
 
 	@Override
 	protected Path compute() {
-		try {
-			if (this.serviceNode.isStructuralNode()) {
-				logger.info("created structural node thread: {}", this.serviceNode.getId());
-				Path retval = processStructuralNode(this.serviceNode, this.inputFile);
-				return retval;
-			} else {
-				logger.info("created branch node thread: {}", this.serviceNode.getId());
-				Path retval = processNonStructuralNode(this.serviceNode, this.inputFile);
-				if (this.workflowStatus.isCancelled()) {
-					addMessage(this.workflowStatus, "Workflow cancelled by user");
-				}
-				return retval;
+		if (this.serviceNode.isStructuralNode()) {
+			logger.info("created structural node thread: {}", this.serviceNode.getId());
+			return processStructuralNode(this.serviceNode, this.inputFile);
+		} else {
+			logger.info("created branch node thread: {}", this.serviceNode.getId());
+			Path retval = processNonStructuralNode(this.serviceNode, this.inputFile);
+			if (this.workflowStatus.isCancelled()) {
+				addMessage(this.workflowStatus, "Workflow cancelled by user");
 			}
-		} catch (ProcessingFailedException e) {
-			this.workflowStatus.addError(e.getMessage());
-			return null;
+			return retval;
 		}
-
 	}
 
 	private Path processStructuralNode(ServiceNode node, Path inputFile) {
 		Path nextInput = inputFile;
-		for (ServiceNode n : node.getChildren()) {
-			try {
-				nextInput = processNonStructuralNode(n, nextInput);
-				// if an error occurs without throwing an exception, successor tasks do not get executed
-				if (n.isFailed()) {
-					break;
-				}
-				if (this.workflowStatus.isCancelled()) {
-					addMessage(this.workflowStatus, "Workflow cancelled by user");
-					break;
-				}
-			} catch (ProcessingFailedException e) {
-				this.workflowStatus.addError(e.getMessage());
+		for (ServiceNode childNode : node.getChildren()) {
+			nextInput = processNonStructuralNode(childNode, nextInput);
+			
+			// if an error occurs without throwing an exception, successor tasks do not get executed
+			if (childNode.isFailed()) {
+				break;
+			}
+			
+			if (this.workflowStatus.isCancelled()) {
+				addMessage(this.workflowStatus, "Workflow cancelled by user");
 				break;
 			}
 		}
@@ -105,52 +101,53 @@
 	}
 
 	private Path processNonStructuralNode(ServiceNode node, Path inputFile) {
-		if (node.getChildren().isEmpty()) {
-			return processLeafNode(node, inputFile);
-		} else if (node.getChildren().size() > 1) {
-			try {
-				Path currOutput = executeCloudService(node, inputFile);
-				// if an error occurs without throwing an exception, successor tasks do not get executed
-				if (node.isFailed()) {
-					return null;
-				}
-				List<ServiceNodeProcessingTask> lActiveProcessingTask = new ArrayList<>();
-				for (ServiceNode n : node.getChildren()) {
-					lActiveProcessingTask.add(new ServiceNodeProcessingTask(n, currOutput, storageService,
-							messagingTemplate, uuid, workflowStatus));
-				}
-				logger.info("launching new branches");
-				ForkJoinTask.invokeAll(lActiveProcessingTask);
-				return currOutput;
-			} catch (ProcessingFailedException e) {
-				this.workflowStatus.addError(e.getMessage());
-				return null;
+		
+		// first execute the service of the provided node
+		Path currOutput = null;
+		try {
+			currOutput = executeCloudService(node, inputFile);
+		} catch (ProcessingFailedException e) {
+			addError(this.workflowStatus, e.getMessage(), node);
+		}
+		
+		// check if the service node execution failed
+		// if an error occurred without throwing an exception, successor tasks do not get executed
+		if (node.isFailed()) {
+			return null;
+		}
+		
+		if (node.getChildren().size() > 1) {
+			ArrayList<ServiceNodeProcessingTask> childTasks = new ArrayList<>();
+			for (ServiceNode childNode : node.getChildren()) {
+				childTasks.add(new ServiceNodeProcessingTask(
+						childNode, 
+						currOutput, 
+						storageService,
+						messagingTemplate, 
+						uuid, 
+						workflowStatus));
 			}
-		} else {
-			try {
-				Path currOutput = executeCloudService(node, inputFile);
-				// if an error occurs without throwing an exception, successor tasks do not get executed
-				if (node.isFailed()) {
-					return null;
-				}
-				ServiceNode nextNode = node.getChildren().get(0);
-				if (nextNode.isStructuralNode()) {
-					ServiceNodeProcessingTask nextNodeTask = new ServiceNodeProcessingTask(nextNode, currOutput,
-							storageService, messagingTemplate, uuid, workflowStatus);
-					ForkJoinPool.commonPool().invoke(nextNodeTask);
-				} else {
-					processNonStructuralNode(nextNode, currOutput);
-				}
-				return currOutput;
-			} catch (ProcessingFailedException e) {
-				this.workflowStatus.addError(e.getMessage());
-				return null;
+
+			logger.info("launching new branches");
+			ForkJoinTask.invokeAll(childTasks);
+		} else if (node.getChildren().size() == 1) {
+			ServiceNode nextNode = node.getChildren().get(0);
+			if (nextNode.isStructuralNode()) {
+				ServiceNodeProcessingTask nextNodeTask = 
+						new ServiceNodeProcessingTask(
+								nextNode, 
+								currOutput,
+								storageService,
+								messagingTemplate,
+								uuid,
+								workflowStatus);
+				ForkJoinPool.commonPool().invoke(nextNodeTask);
+			} else {
+				processNonStructuralNode(nextNode, currOutput);
 			}
 		}
-	}
-
-	private Path processLeafNode(ServiceNode node, Path inputFile) {
-		return executeCloudService(node, inputFile);
+		
+		return currOutput;
 	}
 
 	private Path executeCloudService(ServiceNode node, Path inputFile) {
@@ -160,6 +157,10 @@
 			String serviceName = csd.getName();
 			String baseUrl = csd.getBaseUrl();
 			
+			if (workflowStatus.isCancelled()) {
+				return null;
+			}
+			
 			// upload to service
 			MultipartBody multipartBody = Unirest.post(baseUrl)
 					.field("file", Files.newInputStream(inputFile), inputFile.getFileName().toString());
@@ -196,11 +197,13 @@
 				// error
 				Object body = uploadResponse.getBody();
 				if (body != null && !body.toString().isEmpty()) {
-					workflowStatus.addError("Upload to " + serviceName + " failed! Error code: " + uploadResponse.getStatus() + " - " + body +" - Workflow stopped!");
-					node.markFailed();
+					addError(workflowStatus,
+							"Upload to " + serviceName + " failed! Error code: " + uploadResponse.getStatus() + " - " + body +" - Workflow stopped!",
+							node);
 				} else {
-					workflowStatus.addError("Upload to " + serviceName + " failed! Error code: " + uploadResponse.getStatus() + " - Workflow stopped!");
-					node.markFailed();
+					addError(workflowStatus,
+							"Upload to " + serviceName + " failed! Error code: " + uploadResponse.getStatus() + " - Workflow stopped!",
+							node);
 				}
 				return null;
 			}
@@ -331,16 +334,14 @@
 					}
 					
 					if (error) {
-						workflowStatus.addError(serviceName + " failed with errors");						
-						node.markFailed();
+						addError(workflowStatus, serviceName + " failed with errors", node);						
 					} else {
 						addMessage(workflowStatus, serviceName + " successfull");
 					}
 				} else {
 					String errorUrl = HeaderHelper.getUrlFromLink(linkHeaders, "error", baseUrl);
 					if (errorUrl != null) {
-						workflowStatus.addError(serviceName + " processing finished with error");
-						node.markFailed();
+						addError(workflowStatus, serviceName + " processing finished with error", node);
 
 						// download error file
 						HttpResponse<File> errorResponse = Unirest.get(errorUrl)
@@ -361,8 +362,7 @@
 						// extract delete
 						deleteUrl = HeaderHelper.getUrlFromLink(errorResponse.getHeaders().get("Link"), "delete", baseUrl);
 					} else {
-						workflowStatus.addError(serviceName + " has no result and no error");
-						node.markFailed();
+						addError(workflowStatus, serviceName + " has no result and no error", node);
 					}
 				}
 
@@ -426,4 +426,8 @@
 		}
 	}
 
+	private void addError(WorkflowStatus workflowStatus, String message, ServiceNode node) {
+		workflowStatus.addError(message);						
+		node.markFailed();
+	}
 }
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelper.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelper.java
index 19810dd..7d54b44 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelper.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelper.java
@@ -14,19 +14,19 @@
 package org.eclipse.app4mc.cloud.manager;
 
 import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
-import java.util.stream.Collectors;
 
 import org.eclipse.app4mc.cloud.manager.administration.CloudServiceDefinition;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.util.StringUtils;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 
+import kong.unirest.HttpResponse;
+import kong.unirest.JsonNode;
 import kong.unirest.Unirest;
 
 public final class WorkflowStatusHelper {
@@ -48,71 +48,45 @@
 	 */
 	public static ServiceConfiguration getConfigurationForService(CloudServiceDefinition csd) {
 		String selected = csd.getKey();
-		ServiceConfiguration config = null;
-		
-		if ("validation".equals(selected)) {
-			// TODO general: check if the selected service has a configuration URL
-			// TODO build configuration based on provided service configuration
-			
-			List<String> allProfiles = new ArrayList<>();
+		ServiceConfiguration config = new ServiceConfiguration(csd.getName());
+
+		if (csd.isConfigurationAvailable()) {
 			try {
-				String baseUrl = csd.getBaseUrl();
-				if (!baseUrl.endsWith("/")) {
-					baseUrl += "/";
+				String configUrl = csd.getBaseUrl();
+				if (!configUrl.endsWith("/")) {
+					configUrl += "/";
 				}
-				List<?> jsonResult = Unirest.get(baseUrl + "profiles").asJson().getBody().getArray().toList();
-				allProfiles = jsonResult.stream().map(Object::toString).collect(Collectors.toList());
+				configUrl += "config";
+				HttpResponse<JsonNode> configResponse = Unirest.get(configUrl).asJson();
+				if (configResponse.getStatus() == 200) {
+					JsonNode configJson = configResponse.getBody();
+					if (configJson != null) {
+						ServiceConfigurationDefinition configDefinition = WorkflowStatusHelper.loadServiceConfigurationDefinition(configJson.toString());
+						configDefinition.getParameterList().forEach(p -> config.addParameter(p));
+						if (configDefinition.getDescription() != null) {
+							config.setServiceDescription(configDefinition.getDescription());
+						}
+					} else {
+						LOG.info("Failed to access configuration resource for " + csd.getName() + ": No JSON body in response");
+					}
+				} else {
+					LOG.info("Failed to access configuration resource for " + csd.getName() + ": " + configResponse.getStatusText());
+				}
 			} catch (Exception e) {
-				// do nothing, we will handle configurations in a different way in the future
+				LOG.info("Failed to access configuration resource for " + csd.getName() + ": " + e.getMessage());
 			}
-			
-			if (allProfiles != null && !allProfiles.isEmpty()) {
-				config = new ServiceConfiguration(csd.getName());
-				
-				ServiceConfigurationParameter validationProfiles = new ServiceConfigurationParameter();
-				validationProfiles.setName("Validation profiles");
-				validationProfiles.setKey("profiles");
-				validationProfiles.setType("String");
-				validationProfiles.setCardinality("multiple");
-				validationProfiles.setPossibleValues(allProfiles);
-				config.addParameter(validationProfiles);
-			}
-		} else if ("rtc_analysis".equals(selected)) {
-			config = new ServiceConfiguration(csd.getName());
-			
-			ServiceConfigurationParameter ascPriorities = new ServiceConfigurationParameter();
-			ascPriorities.setName("Ascending priorities");
-			ascPriorities.setKey("analysis-ascending-priorities");
-			ascPriorities.setType("boolean");
-			config.addParameter(ascPriorities);
-
-			ServiceConfigurationParameter enableFlows = new ServiceConfigurationParameter();
-			enableFlows.setName("Include flow analysis");
-			enableFlows.setKey("analysis-enable-flows");
-			enableFlows.setType("boolean");
-			config.addParameter(enableFlows);
-
-			ServiceConfigurationParameter ignoreOverUtil = new ServiceConfigurationParameter();
-			ignoreOverUtil.setName("Ignore over utilization");
-			ignoreOverUtil.setKey("analysis-ignore-overutilization");
-			ignoreOverUtil.setType("boolean");
-			config.addParameter(ignoreOverUtil);
-
-			ServiceConfigurationParameter timeUnit = new ServiceConfigurationParameter();
-			timeUnit.setName("Time unit");
-			timeUnit.setKey("analysis-time-unit");
-			timeUnit.setType("String");
-			timeUnit.setPossibleValues(Arrays.asList("s", "ms", "us", "ns"));
-			config.addParameter(timeUnit);
 		}
 		
-		// specify default configuration parameter
-		if (config == null) {
-			config = new ServiceConfiguration(csd.getName());
+		if (!StringUtils.isEmpty(csd.getDescription())) {
+			// service description is provided in the manager administration panel, so we
+			// override the value provided by the service itself
+			config.setServiceDescription(csd.getDescription());
 		}
-
+		
+		// specify default configuration parameter for the manager
 		ServiceConfigurationParameter timeOut = new ServiceConfigurationParameter();
 		timeOut.setName("Timeout (in ms)");
+		timeOut.setDescription("The number of milliseconds the workflow should wait for the service to finish.\n-1 means to wait infinitely.");
 		timeOut.setKey("timeout");
 		timeOut.setValue("60000");
 		if ("app4mc_sim".equals(selected)) {
@@ -124,6 +98,7 @@
 		
 		ServiceConfigurationParameter deleteResult = new ServiceConfigurationParameter();
 		deleteResult.setName("Delete result ");
+		deleteResult.setDescription("Flag to configure if the result should be deleted in the cloud service.\nDefault is true to keep the infrastructure clean.");
 		deleteResult.setKey("deleteResult");
 		deleteResult.setValue("true");
 		deleteResult.setType("boolean");
@@ -242,4 +217,24 @@
 		}
 		return null;
 	}
+	
+	/**
+	 * Deserializes the given JSON String.
+	 * 
+	 * @param input The String to deserialize.
+	 * @return The deserialized {@link ServiceConfigurationDefinition}.
+	 */
+	public static ServiceConfigurationDefinition loadServiceConfigurationDefinition(String input) {
+		try {
+			ObjectMapper mapper = new ObjectMapper();
+			
+			SimpleModule module = new SimpleModule();
+			mapper.registerModule(module);
+			
+			return mapper.readValue(input, ServiceConfigurationDefinition.class);
+		} catch (Exception e) {
+			LOG.error("Failed to load service configuration definition", e);
+		}
+		return null;
+	}
 }
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusSerializer.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusSerializer.java
index 553cc36..01d1055 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusSerializer.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusSerializer.java
@@ -1,5 +1,5 @@
 /*********************************************************************************
- * Copyright (c) 2020 Robert Bosch GmbH and others.
+ * Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -31,11 +31,7 @@
 	private static final long serialVersionUID = 8920512086262532108L;
 
 	public WorkflowStatusSerializer() {
-        this(null);
-    }
-
-    public WorkflowStatusSerializer(Class<WorkflowStatus> t) {
-        super(t);
+        super(WorkflowStatus.class);
     }
     
 	@Override
@@ -84,11 +80,16 @@
 		if (!node.isStructuralNode()) {
 			ServiceConfiguration configuration = node.getServiceConfiguration();
 			if (configuration != null) {
-				for (ServiceConfigurationParameter param : configuration.getParameterList()) {
-					if (param.getValue() != null) {
-						gen.writeObjectField(param.getKey(), param.getValue());
-					}
-				}
+				configuration.getParameterList().stream()
+					.sorted((o1, o2) -> o1.getKey().compareTo(o2.getKey()))
+					.filter(p -> p.getValue() != null)
+					.forEach(param -> {
+						try {
+							gen.writeObjectField(param.getKey(), param.getValue());
+						} catch (IOException e) {
+							throw new RuntimeException(e);
+						}
+					});
 			}
 		}
 		
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/ApplicationConfig.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/ApplicationConfig.java
index 263d549..ce3a56c 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/ApplicationConfig.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/ApplicationConfig.java
@@ -62,7 +62,8 @@
 					String name = split[1];
 					String url = split[2];
 					String desc = split.length >= 4 ? split[3] : "";
-					return new CloudServiceDefinition(key, name, url, desc);
+					boolean configAvailable = split.length >= 5 ? Boolean.getBoolean(split[4]) : true;
+					return new CloudServiceDefinition(key, name, url, desc, configAvailable);
 				}).collect(Collectors.toList());
 				definitions.addAll(services);
 			} catch (IOException e) {
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/CloudServiceDefinition.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/CloudServiceDefinition.java
index da9b347..8229b4e 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/CloudServiceDefinition.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/CloudServiceDefinition.java
@@ -1,5 +1,5 @@
 /*********************************************************************************
- * Copyright (c) 2020 Robert Bosch GmbH and others.
+ * Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -19,16 +19,22 @@
 	String name;
 	String baseUrl;
 	String description;
+	boolean configurationAvailable = true;
 	
 	public CloudServiceDefinition() {
 		// empty default constructor
 	}
 	
 	public CloudServiceDefinition(String key, String name, String baseUrl, String description) {
+		this(key, name, baseUrl, description, true);
+	}
+	
+	public CloudServiceDefinition(String key, String name, String baseUrl, String description, boolean configAvailable) {
 		this.key = key;
 		this.name = name;
 		this.baseUrl = baseUrl;
 		this.description = description;
+		this.configurationAvailable = configAvailable;
 	}
 
 	public String getKey() {
@@ -62,5 +68,13 @@
 	public void setDescription(String description) {
 		this.description = description;
 	}
+
+	public boolean isConfigurationAvailable() {
+		return configurationAvailable;
+	}
+
+	public void setConfigurationAvailable(boolean configurationAvailable) {
+		this.configurationAvailable = configurationAvailable;
+	}
 	
 }
diff --git a/manager/src/main/resources/services.txt b/manager/src/main/resources/services.txt
index ac82627..318335e 100644
--- a/manager/src/main/resources/services.txt
+++ b/manager/src/main/resources/services.txt
@@ -1,6 +1,6 @@
-migration;Migration;http://localhost:8080/app4mc/converter/;Migrates an input model file to model version 0.9.9
-validation;Validation;http://localhost:8181/app4mc/validation/;Validates the input model file
-rtc_analysis;RTC Analysis;http://localhost:8081/app4mc/analysis/;Executes Real-Time Calculus (RTC) techniques on a provided Amalthea model to analyse timing properties of heterogeneous embedded systems
+migration;Migration;http://localhost:8080/app4mc/converter/
+validation;Validation;http://localhost:8181/app4mc/validation/
+rtc_analysis;RTC Analysis;http://localhost:8081/app4mc/analysis/
 rtc_interpreter;RTC Evaluation;http://localhost:8082/app4mc/interpreter/rtc/;Converts the results of a RTC Analysis into an input for the Chart Visualizer
 label_core_interpreter;Label per Core Evaluation;http://localhost:8084/app4mc/interpreter/label-per-core/;Parses an Amalthea model to inspect label access operations per core
 label_memory_interpreter;Label per Memory Evaluation;http://localhost:8084/app4mc/interpreter/label-per-memory/;Parses an Amalthea model to inspect label access operations per memory
@@ -8,6 +8,6 @@
 label_size_interpreter;Label Size Evaluation;http://localhost:8084/app4mc/interpreter/label-size/;Parses an Amalthea model to inspect the number of labels grouped by size
 chart_visualizer;Chart Visualizer;http://localhost:8083/app4mc/visualization/barchart/;Visualization of interpreter results
 amlt2inchron;Amalthea -> INCHRON;https://am2inc.dev1.inchron.de/projects;Transforms an Amalthea model to an INCHRON model
-amlt2systemc;Amalthea -> SystemC;http://localhost:8282/app4mc/amlt2systemc/;Transforms an Amalthea model to simulation code
+amlt2systemc;Amalthea -> SystemC;http://localhost:8282/app4mc/amlt2systemc/
 app4mc_sim;APP4MC.Sim;http://139.30.201.29:2323/app4mc/simulation/;Executes simulation and generates a BTF traces
 inchron_btf_visualization;INCHRON BTF Trace Visualization;https://trace.dev1.inchron.de/traces/;Visualization of trace data provided by INCHRON
\ No newline at end of file
diff --git a/manager/src/main/resources/services_online.txt b/manager/src/main/resources/services_online.txt
index 85f8073..98cc92e 100644
--- a/manager/src/main/resources/services_online.txt
+++ b/manager/src/main/resources/services_online.txt
@@ -1,6 +1,6 @@
-migration;Migration;https://app4mc.eclipseprojects.io/app4mc/converter/;Migrates an input model file to model version 0.9.9
-validation;Validation;https://app4mc.eclipseprojects.io/app4mc/validation/;Validates the input model file
-rtc_analysis;RTC Analysis;https://app4mc.eclipseprojects.io/app4mc/analysis/;Executes Real-Time Calculus (RTC) techniques on a provided Amalthea model to analyse timing properties of heterogeneous embedded systems
+migration;Migration;https://app4mc.eclipseprojects.io/app4mc/converter/
+validation;Validation;https://app4mc.eclipseprojects.io/app4mc/validation/
+rtc_analysis;RTC Analysis;https://app4mc.eclipseprojects.io/app4mc/analysis/
 rtc_interpreter;RTC Evaluation;https://app4mc.eclipseprojects.io/app4mc/interpreter/rtc/;Converts the results of a RTC Analysis into an input for the Chart Visualizer
 label_core_interpreter;Label per Core Evaluation;https://app4mc.eclipseprojects.io/app4mc/interpreter/label-per-core/;Parses an Amalthea model to inspect label access operations per core
 label_memory_interpreter;Label per Memory Evaluation;https://app4mc.eclipseprojects.io/app4mc/interpreter/label-per-memory/;Parses an Amalthea model to inspect label access operations per memory
@@ -8,6 +8,6 @@
 label_size_interpreter;Label Size Evaluation;https://app4mc.eclipseprojects.io/app4mc/interpreter/label-size/;Parses an Amalthea model to inspect the number of labels grouped by size
 chart_visualizer;Chart Visualizer;https://app4mc.eclipseprojects.io/app4mc/visualization/barchart/;Visualization of interpreter results
 amlt2inchron;Amalthea -> INCHRON;https://am2inc.dev1.inchron.de/projects;Transforms an Amalthea model to an INCHRON model
-amlt2systemc;Amalthea -> SystemC;https://app4mc.eclipseprojects.io/app4mc/amlt2systemc/;Transforms an Amalthea model to simulation code
+amlt2systemc;Amalthea -> SystemC;https://app4mc.eclipseprojects.io/app4mc/amlt2systemc/
 app4mc_sim;APP4MC.Sim;http://139.30.201.29:2323/app4mc/simulation/;Executes simulation and generates a BTF traces
 inchron_btf_visualization;INCHRON BTF Trace Visualization;https://trace.dev1.inchron.de/traces/;Visualization of trace data provided by INCHRON
\ No newline at end of file
diff --git a/manager/src/main/resources/templates/admin.html b/manager/src/main/resources/templates/admin.html
index 15d2528..06d6bab 100644
--- a/manager/src/main/resources/templates/admin.html
+++ b/manager/src/main/resources/templates/admin.html
@@ -25,6 +25,7 @@
 									<th>Service Name</th>
 									<th>Service Base URL</th>
 									<th>Service Description</th>
+									<th>Service Configuration</th>
 									<th></th>
 								</tr>
 							</thead>
@@ -50,6 +51,13 @@
 											th:field="*{services[__${stat.index}__].description}"
 											class="form-control fm-ctrl-right" />
 									</td>
+									<td class="text-center">
+										<input
+											type="checkbox"
+											th:field="*{services[__${stat.index}__].configurationAvailable}"
+											class="form-check-input"
+											style="text-center" />
+									</td>
 									<td>
 										<a th:if="*{services[__${stat.index}__].name != null}"
 											th:attr="onclick=|removeService('*{services[__${stat.index}__].key}')|"
diff --git a/manager/src/main/resources/templates/selectedServices.html b/manager/src/main/resources/templates/selectedServices.html
index d8383ae..26b0114 100644
--- a/manager/src/main/resources/templates/selectedServices.html
+++ b/manager/src/main/resources/templates/selectedServices.html
@@ -39,29 +39,39 @@
 		<div th:if="not *{selectedServices.isEmpty()}">
 			<div th:each="node, nodeStatus : *{selectedServices}" class="mb-3">
 				<h4 th:text="${node.service.name + ' Configuration'}">Config</h4>
+				<div th:text="${node.serviceConfiguration.serviceDescription}" class="font-italic mb-1">Description</div>
 				<div th:each="parameter, parameterStatus : ${node.serviceConfiguration.parameterList}">
 					<!-- single non-boolean value -->
 					<div th:if="${parameter.cardinality == 'single' and parameter.type != 'boolean' and parameter.possibleValues.isEmpty()}">
 						<label class="form-check-label" th:text="${parameter.name}">Label</label>
 						<input 
-							class="form-control" 
+							class="form-control mb-1" 
 							type="text" 
 							th:field="*{selectedServices[__${nodeStatus.index}__].serviceConfiguration.parameterList[__${parameterStatus.index}__].value}"
-							th:disabled="*{uuid != null}"/>
+							th:disabled="*{uuid != null}"
+					    	th:title="*{selectedServices[__${nodeStatus.index}__].serviceConfiguration.parameterList[__${parameterStatus.index}__].description}"
+							data-toggle="tooltip"/>
 					</div>
 					<!-- single boolean value -->
 					<div th:if="${parameter.cardinality == 'single' and parameter.type == 'boolean'}" class="form-check">
 						<input
-							class="form-check-input"
+							class="form-check-input mb-1"
 							type="checkbox"
 							th:field="*{selectedServices[__${nodeStatus.index}__].serviceConfiguration.parameterList[__${parameterStatus.index}__].value}"
 							th:value="true"
 							th:disabled="*{uuid != null}">
-						<label class="form-check-label" th:text="${parameter.name}" th:for="${parameter.key}">Label</label>
+						<label
+							class="form-check-label"
+							th:text="${parameter.name}"
+							th:for="${parameter.key}"
+					    	th:title="*{selectedServices[__${nodeStatus.index}__].serviceConfiguration.parameterList[__${parameterStatus.index}__].description}"
+							data-toggle="tooltip">Label</label>
 					</div>
 					<!-- multiple possible values + cardinality multiple = checkboxes -->
 					<div th:if="${parameter.cardinality == 'multiple' and parameter.possibleValues.size() > 1}">
-						<label th:text="${parameter.name}">Label</label>
+						<label
+							th:text="${parameter.name}"th:title="*{selectedServices[__${nodeStatus.index}__].serviceConfiguration.parameterList[__${parameterStatus.index}__].description}"
+							data-toggle="tooltip">Label</label>
 						<div th:each="pv : ${parameter.possibleValues}" class="form-check">
 							<input
 								type="checkbox"
@@ -75,9 +85,11 @@
 					<div th:if="${parameter.cardinality == 'single' and parameter.possibleValues.size() > 1}">
 						<label th:text="${parameter.name}">Label</label>
 						<select
-							class="custom-select"
+							class="form-control mb-1"
 							th:field="*{selectedServices[__${nodeStatus.index}__].serviceConfiguration.parameterList[__${parameterStatus.index}__].value}"
-							th:disabled="*{uuid != null}">
+							th:disabled="*{uuid != null}"
+							th:title="*{selectedServices[__${nodeStatus.index}__].serviceConfiguration.parameterList[__${parameterStatus.index}__].description}"
+							data-toggle="tooltip">
 							<option value=""></option>
 						    <option 
 						    	th:each="pv : ${parameter.possibleValues}"  
diff --git a/manager/src/test/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelperTest.java b/manager/src/test/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelperTest.java
index 19dd243..3681757 100644
--- a/manager/src/test/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelperTest.java
+++ b/manager/src/test/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelperTest.java
@@ -20,7 +20,12 @@
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import java.util.ArrayList;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -44,39 +49,46 @@
 			"migration", 
 			"Migration", 
 			"http://localhost:8080/app4mc/converter/", 
-			"Migrates an input model file to model version 0.9.9");
+			"Migrates an input model file to model version 0.9.9",
+			false);
 	private static CloudServiceDefinition VALIDATION = new CloudServiceDefinition(
 			"validation", 
 			"Validation", 
 			"http://localhost:8181/app4mc/validation/", 
-			"Validates the input model file");
+			"Validates the input model file",
+			false);
 
 	private static CloudServiceDefinition LABEL_CORE = new CloudServiceDefinition(
 			"label_core_interpreter", 
 			"Label per Core Interpreter", 
 			"http://localhost:8084/app4mc/interpreter/label-per-core/", 
-			"Parses an Amalthea model to inspect label access operations per core");
+			"Parses an Amalthea model to inspect label access operations per core",
+			false);
 	private static CloudServiceDefinition LABEL_MEMORY = new CloudServiceDefinition(
 			"label_memory_interpreter", 
 			"Label per Memory Interpreter", 
 			"http://localhost:8084/app4mc/interpreter/label-per-memory/", 
-			"Parses an Amalthea model to inspect label access operations per memory");
+			"Parses an Amalthea model to inspect label access operations per memory",
+			false);
 	private static CloudServiceDefinition LABEL_TASK = new CloudServiceDefinition(
 			"label_task_interpreter", 
 			"Label per Task Interpreter", 
 			"http://localhost:8084/app4mc/interpreter/label-per-task/", 
-			"Parses an Amalthea model to inspect label access operations per task");
+			"Parses an Amalthea model to inspect label access operations per task",
+			false);
 	private static CloudServiceDefinition LABEL_SIZE = new CloudServiceDefinition(
 			"label_size_interpreter", 
 			"Label Size Interpreter", 
 			"http://localhost:8084/app4mc/interpreter/label-size/", 
-			"Parses an Amalthea model to inspect the number of labels grouped by size");
+			"Parses an Amalthea model to inspect the number of labels grouped by size",
+			false);
 	
 	private static CloudServiceDefinition CHART_VISUALIZER = new CloudServiceDefinition(
 			"chart_visualizer", 
 			"Chart Visualizer", 
 			"http://localhost:8083/app4mc/visualization/barchart/", 
-			"Visualization of interpreter results");
+			"Visualization of interpreter results",
+			false);
 
 	@BeforeAll
 	public static void beforeAll() {
@@ -510,4 +522,165 @@
 
 		assertEquals(1, serviceNode.getChildren().size());
 	}
+	
+	@Test
+	public void shouldDeserializeMigrationServiceDefinition() throws IOException {
+		Path resourceDirectory = Paths.get("src", "test", "resources", "conf_def_migration.json");
+		StringBuilder builder = new StringBuilder();
+	    try (BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(resourceDirectory)))) {
+	        String line;
+	        while ((line = br.readLine()) != null) {
+	            builder.append(line).append("\n");
+	        }
+	    }
+	    
+	    ServiceConfigurationDefinition definition = 
+	    		WorkflowStatusHelper.loadServiceConfigurationDefinition(builder.toString());
+	    
+	    assertEquals("Migrates an input model file to model version 1.0.0", definition.getDescription());
+	    
+	    assertEquals("amxmi", definition.getInputType());
+	    assertNull(definition.getInputVersion());
+	    assertTrue(definition.isInputArchiveSupported());
+
+	    assertEquals("amxmi", definition.getOutputType());
+	    assertEquals("1.0.0", definition.getOutputVersion());
+	    assertTrue(definition.isOutputArchiveSupported());
+	    
+	    assertEquals(1, definition.getParameterList().size());
+	    
+	    ServiceConfigurationParameter versionParam = definition.getParameter("version");
+	    assertNotNull(versionParam);
+	    assertEquals("version", versionParam.getKey());
+	    assertEquals("Output Model Version", versionParam.getName());
+	    assertEquals("The model version to which the input should be migrated to", versionParam.getDescription());
+	    assertEquals("1.0.0", versionParam.getValue());
+	    assertEquals("String", versionParam.getType());
+	    assertEquals("single", versionParam.getCardinality());
+	    assertFalse(versionParam.isMandatory());
+	    assertFalse(versionParam.isManagerParameter());
+	    
+	    List<String> possibleValues = versionParam.getPossibleValues();
+	    assertNotNull(possibleValues);
+	    assertEquals(4, possibleValues.size());
+	    assertTrue(possibleValues.contains("1.0.0"));
+	    assertTrue(possibleValues.contains("0.9.9"));
+	    assertTrue(possibleValues.contains("0.9.8"));
+	    assertTrue(possibleValues.contains("0.9.7"));
+	}
+	
+	@Test
+	public void shouldDeserializeValidationServiceDefinition() throws IOException {
+		Path resourceDirectory = Paths.get("src", "test", "resources", "conf_def_validation.json");
+		StringBuilder builder = new StringBuilder();
+		try (BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(resourceDirectory)))) {
+			String line;
+			while ((line = br.readLine()) != null) {
+				builder.append(line).append("\n");
+			}
+		}
+		
+		ServiceConfigurationDefinition definition = 
+				WorkflowStatusHelper.loadServiceConfigurationDefinition(builder.toString());
+		
+		assertEquals("amxmi", definition.getInputType());
+		assertEquals("1.0.0", definition.getInputVersion());
+		assertTrue(definition.isInputArchiveSupported());
+		
+		assertNull(definition.getOutputType());
+		assertNull(definition.getOutputVersion());
+		assertFalse(definition.isOutputArchiveSupported());
+		
+		assertEquals(1, definition.getParameterList().size());
+		
+		ServiceConfigurationParameter profilesParam = definition.getParameter("profiles");
+		assertNotNull(profilesParam);
+		assertEquals("profiles", profilesParam.getKey());
+		assertEquals("Validation profiles", profilesParam.getName());
+		assertNull(profilesParam.getValue());
+		assertEquals("String", profilesParam.getType());
+		assertEquals("multiple", profilesParam.getCardinality());
+		assertFalse(profilesParam.isMandatory());
+		assertFalse(profilesParam.isManagerParameter());
+		
+		List<String> possibleValues = profilesParam.getPossibleValues();
+		assertNotNull(possibleValues);
+		assertEquals(3, possibleValues.size());
+		assertTrue(possibleValues.contains("Amalthea"));
+		assertTrue(possibleValues.contains("INCHRON"));
+		assertTrue(possibleValues.contains("Timing Architects"));
+	}
+	
+	@Test
+	public void shouldDeserializeRtcServiceDefinition() throws IOException {
+		Path resourceDirectory = Paths.get("src", "test", "resources", "conf_def_rtc.json");
+		StringBuilder builder = new StringBuilder();
+		try (BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(resourceDirectory)))) {
+			String line;
+			while ((line = br.readLine()) != null) {
+				builder.append(line).append("\n");
+			}
+		}
+		
+		ServiceConfigurationDefinition definition = 
+				WorkflowStatusHelper.loadServiceConfigurationDefinition(builder.toString());
+		
+		assertEquals("amxmi", definition.getInputType());
+		assertEquals("1.0.0", definition.getInputVersion());
+		assertFalse(definition.isInputArchiveSupported());
+		
+		assertEquals("json", definition.getOutputType());
+		assertNull(definition.getOutputVersion());
+		assertFalse(definition.isOutputArchiveSupported());
+		
+		assertEquals(4, definition.getParameterList().size());
+		
+		ServiceConfigurationParameter param = definition.getParameter("analysis-ascending-priorities");
+		assertNotNull(param);
+		assertEquals("analysis-ascending-priorities", param.getKey());
+		assertEquals("Ascending priorities", param.getName());
+		assertNull(param.getValue());
+		assertEquals("boolean", param.getType());
+		assertEquals("single", param.getCardinality());
+		assertFalse(param.isMandatory());
+		assertFalse(param.isManagerParameter());
+		
+		param = definition.getParameter("analysis-enable-flows");
+		assertNotNull(param);
+		assertEquals("analysis-enable-flows", param.getKey());
+		assertEquals("Include flow analysis", param.getName());
+		assertNull(param.getValue());
+		assertEquals("boolean", param.getType());
+		assertEquals("single", param.getCardinality());
+		assertFalse(param.isMandatory());
+		assertFalse(param.isManagerParameter());
+		
+		param = definition.getParameter("analysis-ignore-overutilization");
+		assertNotNull(param);
+		assertEquals("analysis-ignore-overutilization", param.getKey());
+		assertEquals("Ignore over utilization", param.getName());
+		assertNull(param.getValue());
+		assertEquals("boolean", param.getType());
+		assertEquals("single", param.getCardinality());
+		assertFalse(param.isMandatory());
+		assertFalse(param.isManagerParameter());
+		
+		param = definition.getParameter("analysis-time-unit");
+		assertNotNull(param);
+		assertEquals("analysis-time-unit", param.getKey());
+		assertEquals("Time unit", param.getName());
+		assertNull(param.getValue());
+		assertEquals("String", param.getType());
+		assertEquals("single", param.getCardinality());
+		assertFalse(param.isMandatory());
+		assertFalse(param.isManagerParameter());
+		
+		List<String> possibleValues = param.getPossibleValues();
+		assertNotNull(possibleValues);
+		assertEquals(4, possibleValues.size());
+		assertTrue(possibleValues.contains("s"));
+		assertTrue(possibleValues.contains("ms"));
+		assertTrue(possibleValues.contains("us"));
+		assertTrue(possibleValues.contains("ns"));
+	}
 }
diff --git a/manager/src/test/resources/conf_def_migration.json b/manager/src/test/resources/conf_def_migration.json
new file mode 100644
index 0000000..c894d25
--- /dev/null
+++ b/manager/src/test/resources/conf_def_migration.json
@@ -0,0 +1,20 @@
+{
+  "description" : "Migrates an input model file to model version 1.0.0",
+  "input" : {
+    "type" : "amxmi",
+    "archive-supported" : true
+  },
+  "output" : {
+    "type" : "amxmi",
+    "version" : "1.0.0",
+    "archive-supported" : true
+  },
+  "parameter" : {
+    "version" : {
+      "name" : "Output Model Version",
+      "description" : "The model version to which the input should be migrated to",
+      "value" : "1.0.0",
+      "values" : ["1.0.0", "0.9.9", "0.9.8", "0.9.7"]
+    }
+  }
+}
\ No newline at end of file
diff --git a/manager/src/test/resources/conf_def_rtc.json b/manager/src/test/resources/conf_def_rtc.json
new file mode 100644
index 0000000..842f360
--- /dev/null
+++ b/manager/src/test/resources/conf_def_rtc.json
@@ -0,0 +1,29 @@
+{
+  "input" : {
+    "type" : "amxmi",
+    "version" : "1.0.0",
+    "archive-supported" : false
+  },
+  "output" : {
+    "type" : "json",
+    "archive-supported" : false
+  },
+  "parameter" : {
+    "analysis-ascending-priorities" : {
+      "name" : "Ascending priorities",
+      "type" : "boolean"
+    },
+    "analysis-enable-flows" : {
+      "name" : "Include flow analysis",
+      "type" : "boolean"
+    },
+    "analysis-ignore-overutilization" : {
+      "name" : "Ignore over utilization",
+      "type" : "boolean"
+    },
+    "analysis-time-unit" : {
+      "name" : "Time unit",
+      "values" : ["s", "ms", "us", "ns"]
+    }
+  }
+}
\ No newline at end of file
diff --git a/manager/src/test/resources/conf_def_validation.json b/manager/src/test/resources/conf_def_validation.json
new file mode 100644
index 0000000..c6ccc62
--- /dev/null
+++ b/manager/src/test/resources/conf_def_validation.json
@@ -0,0 +1,14 @@
+{
+  "input" : {
+    "type" : "amxmi",
+    "version" : "1.0.0",
+    "archive-supported" : true
+  },
+  "parameter" : {
+    "profiles" : {
+      "name" : "Validation profiles",
+      "cardinality" : "multiple",
+      "values" : ["Amalthea", "INCHRON", "Timing Architects"]
+    }
+  }
+}
\ No newline at end of file
diff --git a/org.eclipse.app4m.amlt2systemc.cloud/openapi.yaml b/org.eclipse.app4m.amlt2systemc.cloud/openapi.yaml
new file mode 100644
index 0000000..3e41242
--- /dev/null
+++ b/org.eclipse.app4m.amlt2systemc.cloud/openapi.yaml
@@ -0,0 +1,198 @@
+openapi: 3.0.0
+info:
+  version: 0.1.0
+  title: APP4MC amlt to SystemC Transformation API
+  description: APP4MC Transformation API to transform an Amalthea model file into simulation code.
+
+servers:
+  - url: http://localhost:8080/app4mc/validation
+  - url: https://app4mc.eclipseprojects.io/app4mc/amlt2systemc
+  
+paths:
+  /:
+    post:
+      summary: Upload the file to transform and start the transformation process asynchronously
+      requestBody:
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                file:
+                  type: string
+                  format: binary
+      responses:
+        '201':
+          description: Upload succeeded and transformation process started
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  id:
+                    type: string
+                    description: ID of the created transformation resource
+          headers:
+            Location:
+              schema:
+                type: string
+                format: uri
+              description: The URI to the status URL
+          links:
+            status:
+              operationId: getStatus
+              parameters:
+                statusId: '$response.body#/id'
+              description: >
+                The `id` value returned in the response can be used as
+                the `statusId` parameter in `GET /{statusId}`.
+        '400':
+          description: No model file provided as upload
+        '404':
+          description: Upload failed
+
+  /{statusId}:
+    get:
+      summary: Get the status of the triggered transformation process
+      operationId: getStatus
+      parameters:
+        - in: path
+          name: statusId
+          required: true
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Processing finished successfully
+          headers:
+            Cache-Control:
+              schema:
+                type: string
+                enum:
+                  - private, no-store, no-cache, must-revalidate
+          links:
+            result:
+              operationId: getDownload
+              parameters:
+                statusId: $request.path.statusId
+        '202':
+          description: Transformation process in progress
+          headers:
+            Cache-Control:
+              schema:
+                type: string
+                enum:
+                  - private, no-store, no-cache, must-revalidate
+        '204':
+          description: Processing finished with an error
+          headers:
+            Cache-Control:
+              schema:
+                type: string
+                enum:
+                  - private, no-store, no-cache, must-revalidate
+          links:
+            result:
+              operationId: getError
+              parameters:
+                statusId: $request.path.statusId
+        '404':
+          description: Resource not available
+
+    delete:
+      summary: Delete the uploaded and result resource from the server
+      operationId: deleteResource
+      parameters:
+        - in: path
+          name: statusId
+          required: true
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Resource deleted successfully
+        '404':
+          description: Resource not available
+
+  /{statusId}/download:
+    get:
+      summary: Download the transformation result file
+      operationId: getDownload
+      parameters:
+        - in: path
+          name: statusId
+          required: true
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Transformation successful
+          content:
+            application/octet-stream:
+              schema:
+                type: string
+                format: binary
+          links:
+            delete:
+              operationId: deleteResource
+              parameters:
+                statusId: $request.path.statusId
+        '400':
+          description: Transformation finished with errors
+          content:
+            application/octet-stream:
+              schema:
+                type: string
+                format: binary
+          links:
+            delete:
+              operationId: deleteResource
+              parameters:
+                statusId: $request.path.statusId
+        '404':
+          description: Progress still running / Resource not available / No result available
+          content:
+            application/json:
+              schema:
+                type: string
+
+  /{statusId}/error:
+    get:
+      summary: Download the error file of the transformation process
+      operationId: getError
+      parameters:
+        - in: path
+          name: statusId
+          required: true
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Error occured
+          content:
+            application/octet-stream:
+              schema:
+                type: string
+                format: binary
+          links:
+            delete:
+              operationId: deleteResource
+              parameters:
+                statusId: $request.path.statusId
+        '404':
+          description: Resource not available / No error occured
+          content:
+            application/json:
+              schema:
+                type: string
+
+  /config:
+    get:
+      summary: Get the configuration definition of the service
+      responses:
+        '200':
+          description: The configuration definition of the service
+          content:
+            application/json:
+              schema:
+                type: string
diff --git a/org.eclipse.app4m.amlt2systemc.cloud/org.eclipse.app4mc.amlt2systemc.cloud.http/META-INF/MANIFEST.MF b/org.eclipse.app4m.amlt2systemc.cloud/org.eclipse.app4mc.amlt2systemc.cloud.http/META-INF/MANIFEST.MF
index abc415b..de0d068 100644
--- a/org.eclipse.app4m.amlt2systemc.cloud/org.eclipse.app4mc.amlt2systemc.cloud.http/META-INF/MANIFEST.MF
+++ b/org.eclipse.app4m.amlt2systemc.cloud/org.eclipse.app4mc.amlt2systemc.cloud.http/META-INF/MANIFEST.MF
@@ -7,8 +7,10 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: com.fasterxml.jackson.core;version="2.9.9",
  com.fasterxml.jackson.databind;version="2.9.93",
+ com.fasterxml.jackson.databind.node;version="2.9.93",
  javax.servlet;version="3.1.0",
  javax.servlet.http;version="3.1.0",
+ org.eclipse.app4mc.amalthea.model,
  org.osgi.framework;version="1.9.0",
  org.osgi.service.component.annotations;version="1.3.0";resolution:=optional,
  org.osgi.service.event;version="1.4.0",
diff --git a/org.eclipse.app4m.amlt2systemc.cloud/org.eclipse.app4mc.amlt2systemc.cloud.http/src/org/eclipse/app4mc/amlt2systemc/cloud/http/TransformationServlet.java b/org.eclipse.app4m.amlt2systemc.cloud/org.eclipse.app4mc.amlt2systemc.cloud.http/src/org/eclipse/app4mc/amlt2systemc/cloud/http/TransformationServlet.java
index 2051956..aa74ed2 100644
--- a/org.eclipse.app4m.amlt2systemc.cloud/org.eclipse.app4mc.amlt2systemc.cloud.http/src/org/eclipse/app4mc/amlt2systemc/cloud/http/TransformationServlet.java
+++ b/org.eclipse.app4m.amlt2systemc.cloud/org.eclipse.app4mc.amlt2systemc.cloud.http/src/org/eclipse/app4mc/amlt2systemc/cloud/http/TransformationServlet.java
@@ -1,5 +1,5 @@
 /*********************************************************************************
- * Copyright (c) 2020 Robert Bosch GmbH and others.
+ * Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -38,6 +38,7 @@
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.Part;
 
+import org.eclipse.app4mc.amalthea.model.AmaltheaPackage;
 import org.eclipse.app4mc.transformation.ServiceConstants;
 import org.eclipse.app4mc.transformation.TransformationProcessor;
 import org.osgi.framework.BundleContext;
@@ -53,6 +54,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
 @Component(
     service=Servlet.class,
     property= {
@@ -74,6 +79,18 @@
 	
 	private static final String ERROR_FILE = "error.txt";
 
+	private static final String MODEL_VERSION;
+	static {
+		// Extracting namespace from AmaltheaPackage
+		String nsURI = AmaltheaPackage.eNS_URI;
+		
+		// Extracting AMALTHEA metamodel version
+		MODEL_VERSION = nsURI.lastIndexOf('/') != -1
+				? nsURI.substring(nsURI.lastIndexOf('/') + 1)
+				: nsURI;
+
+	}
+
     private final String defaultBaseDir = System.getProperty("java.io.tmpdir");
 	
 	private ExecutorService executor = Executors.newFixedThreadPool(1);
@@ -212,6 +229,7 @@
     	return;
     }
     
+    // GET /app4mc/amlt2systemc/config
     // GET /app4mc/amlt2systemc/{id}
     // GET /app4mc/amlt2systemc/{id}/download
     // GET /app4mc/amlt2systemc/{id}/error
@@ -229,7 +247,33 @@
     		return;    		
     	}
 
-    	if (splitPath.length == 2) {
+    	if (splitPath.length == 2 && "config".equals(splitPath[1])) {
+    		response.setContentType("application/json");
+    		ObjectMapper mapper = new ObjectMapper();
+    		
+    		ObjectNode config = mapper.createObjectNode();
+    		config.put("description", "Transform an Amalthea model to simulation code.");
+    		
+    		ObjectNode input = mapper.createObjectNode();
+    		input.put("type", "amxmi");
+    		input.put("version", MODEL_VERSION);
+    		input.put("archive-supported", true);
+    		config.set("input", input);
+
+    		ObjectNode output = mapper.createObjectNode();
+    		output.put("type", "app4mc-sim-model");
+    		output.put("archive-supported", true);
+    		config.set("output", output);
+
+    		try {
+    			String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(config);
+    			response.getWriter().write(json);
+    		} catch (JsonProcessingException e) {
+    			response.getWriter().write(mapper.writeValueAsString(e));
+    		}
+
+    		return;
+    	} else if (splitPath.length == 2) {
     		response.setHeader("Cache-Control", "private, no-store, no-cache, must-revalidate");
 
     		String uuid = splitPath[1];
diff --git a/org.eclipse.app4mc.converter.cloud/converter-service/src/main/java/org/eclipse/app4mc/org/eclipse/app4mc/converter/cloud/MigrationRestService.java b/org.eclipse.app4mc.converter.cloud/converter-service/src/main/java/org/eclipse/app4mc/org/eclipse/app4mc/converter/cloud/MigrationRestService.java
index 6800b89..c438c52 100644
--- a/org.eclipse.app4mc.converter.cloud/converter-service/src/main/java/org/eclipse/app4mc/org/eclipse/app4mc/converter/cloud/MigrationRestService.java
+++ b/org.eclipse.app4mc.converter.cloud/converter-service/src/main/java/org/eclipse/app4mc/org/eclipse/app4mc/converter/cloud/MigrationRestService.java
@@ -1,5 +1,5 @@
 /*********************************************************************************
- * Copyright (c) 2020 Robert Bosch GmbH and others.
+ * Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -13,13 +13,16 @@
  */
 package org.eclipse.app4mc.org.eclipse.app4mc.converter.cloud;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
@@ -64,6 +67,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
 @Component(service=MigrationRestService.class)
 @JaxrsResource
 @Produces(MediaType.APPLICATION_JSON)
@@ -88,6 +96,47 @@
 	@Reference
 	MigrationProcessor migrationProcessor;
 	
+	@GET
+	@Path("config")
+	public String config() {
+		ObjectMapper mapper = new ObjectMapper();
+		
+		ObjectNode config = mapper.createObjectNode();
+		config.put("description", "Migrate the input model file to the specified output model version.");
+		
+		ObjectNode input = mapper.createObjectNode();
+		input.put("type", "amxmi");
+		input.put("archive-supported", true);
+		config.set("input", input);
+
+		ObjectNode output = mapper.createObjectNode();
+		output.put("type", "amxmi");
+		output.put("version", ModelVersion.getLatestVersion());
+		output.put("archive-supported", true);
+		config.set("output", output);
+
+		ObjectNode parameter = mapper.createObjectNode();
+		ObjectNode version = mapper.createObjectNode();
+		version.put("name", "Output Model Version");
+		version.put("description", "The model version to which the input should be migrated to.");
+		// the default value to use is the latest supported version
+		version.put("value", ModelVersion.getLatestVersion());
+		ArrayNode values = version.putArray("values");
+		
+		List<String> versions = ModelVersion.getAllSupportedVersions();
+		Collections.reverse(versions);
+		versions.forEach(v -> values.add(v));
+		
+		parameter.set("version", version);
+		config.set("parameter", parameter);
+	
+		try {
+			return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(config);
+		} catch (JsonProcessingException e) {
+			return "Error in generating configuration definition: " + e.getMessage();
+		}
+	}
+	
     @POST
     @Consumes(MediaType.MULTIPART_FORM_DATA)
     public Response upload(@Context HttpServletRequest request, @Context UriInfo uriInfo, @Context ServletContext context) throws IOException, ServletException {
@@ -111,11 +160,12 @@
     				// mark uuid in progress
     				getRegistry(context).put(uuid, PROGRESS_MARKER);
     				
+    				// check if the output model version was provided as parameter
+    				String outputModelVersion = getOutputModelVersion(request);
+    				
     				// trigger asynchronous processing
     				executor.execute(() -> {
     					
-    					String outputModelVersion = ModelVersion.getLatestVersion();
-    					
     					try (MigrationSettings migrationSettings = new MigrationSettings()) {
     						migrationSettings.setProject(uploaded.getParent().toFile());
     						migrationSettings.setMigrationModelVersion(outputModelVersion);
@@ -157,6 +207,19 @@
     			.build();
     }
     
+    private String getOutputModelVersion(HttpServletRequest request) throws IOException, ServletException {
+		String outputModelVersion = ModelVersion.getLatestVersion();
+
+		Part versionPart = request.getPart("version");
+		if (versionPart != null) {
+			try (BufferedReader reader = new BufferedReader(new InputStreamReader(versionPart.getInputStream()))) {
+				outputModelVersion = reader.readLine();
+			}
+		}
+
+		return outputModelVersion;
+    }
+    
     @Path("{uuid}")
     @GET
     public Response status(@PathParam("uuid") String uuid, @Context UriInfo uriInfo, @Context ServletContext context) throws IOException {
diff --git a/org.eclipse.app4mc.converter.cloud/openapi.yaml b/org.eclipse.app4mc.converter.cloud/openapi.yaml
index 78d0491..07bdbaa 100644
--- a/org.eclipse.app4mc.converter.cloud/openapi.yaml
+++ b/org.eclipse.app4mc.converter.cloud/openapi.yaml
@@ -1,11 +1,12 @@
 openapi: 3.0.0
 info:
-  version: 0.9.9
+  version: 1.0.0
   title: APP4MC Migration API
   description: APP4MC Model Migration API to migrate older model versions to the current one
 
 servers:
   - url: http://localhost:8080/app4mc/converter
+  - url: https://app4mc.eclipseprojects.io/app4mc/converter
   
 paths:
   /:
@@ -20,6 +21,9 @@
                 file:
                   type: string
                   format: binary
+                version:
+                  type: string
+                  description: The version to which the model should be converted to. Parameter is optional and defaults to the latest supported version in this service.
       responses:
         '201':
           description: Upload succeeded and migration process started
@@ -172,3 +176,14 @@
             application/json:
               schema:
                 type: string
+
+  /config:
+    get:
+      summary: Get the configuration definition of the service
+      responses:
+        '200':
+          description: The configuration definition of the service
+          content:
+            application/json:
+              schema:
+                type: string
diff --git a/org.eclipse.app4mc.validation.cloud/openapi.yaml b/org.eclipse.app4mc.validation.cloud/openapi.yaml
index 61f8369..44aed55 100644
--- a/org.eclipse.app4mc.validation.cloud/openapi.yaml
+++ b/org.eclipse.app4mc.validation.cloud/openapi.yaml
@@ -1,11 +1,12 @@
 openapi: 3.0.0
 info:
-  version: 0.9.9
+  version: 1.0.0
   title: APP4MC Validation API
   description: APP4MC Validation API to validate an Amalthea model file
 
 servers:
   - url: http://localhost:8080/app4mc/validation
+  - url: https://app4mc.eclipseprojects.io/app4mc/validation
   
 paths:
   /:
@@ -20,6 +21,9 @@
                 file:
                   type: string
                   format: binary
+                profiles:
+                  type: string
+                  description: The validation profiles to execute. Parameter is optional and defaults to the Amalthea Standard Validations profile.
       responses:
         '201':
           description: Upload succeeded and validation process started
@@ -171,7 +175,7 @@
 
   /{statusId}/error:
     get:
-      summary: Download the error file of the migration process
+      summary: Download the error file of the validation process
       operationId: getError
       parameters:
         - in: path
@@ -198,3 +202,14 @@
             application/json:
               schema:
                 type: string
+
+  /config:
+    get:
+      summary: Get the configuration definition of the service
+      responses:
+        '200':
+          description: The configuration definition of the service
+          content:
+            application/json:
+              schema:
+                type: string
diff --git a/org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.http/META-INF/MANIFEST.MF b/org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.http/META-INF/MANIFEST.MF
index 799db2f..0e57afe 100644
--- a/org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.http/META-INF/MANIFEST.MF
+++ b/org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.http/META-INF/MANIFEST.MF
@@ -7,6 +7,7 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: com.fasterxml.jackson.core;version="2.9.9",
  com.fasterxml.jackson.databind;version="2.9.93",
+ com.fasterxml.jackson.databind.node;version="2.9.93",
  javax.servlet;version="3.1.0",
  javax.servlet.http;version="3.1.0",
  org.osgi.service.component.annotations;version="[1.3.0,1.4.0)";resolution:=optional,
diff --git a/org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.http/src/org/eclipse/app4mc/validation/cloud/http/ValidationServlet.java b/org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.http/src/org/eclipse/app4mc/validation/cloud/http/ValidationServlet.java
index 8d02b69..a7f2499 100644
--- a/org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.http/src/org/eclipse/app4mc/validation/cloud/http/ValidationServlet.java
+++ b/org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.http/src/org/eclipse/app4mc/validation/cloud/http/ValidationServlet.java
@@ -1,5 +1,5 @@
 /*********************************************************************************
- * Copyright (c) 2020 Robert Bosch GmbH and others.
+ * Copyright (c) 2020, 2021 Robert Bosch GmbH and others.
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -45,6 +45,7 @@
 import javax.servlet.http.Part;
 
 import org.eclipse.app4mc.amalthea.model.Amalthea;
+import org.eclipse.app4mc.amalthea.model.AmaltheaPackage;
 import org.eclipse.app4mc.amalthea.model.io.AmaltheaLoader;
 import org.eclipse.app4mc.validation.core.IProfile;
 import org.eclipse.app4mc.validation.util.ProfileManager;
@@ -57,6 +58,8 @@
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 
 @Component(
     service=Servlet.class,
@@ -82,6 +85,18 @@
 	private static final String PASSED_MARKER = "passed";
 	private static final String FAILED_MARKER = "failed";
 
+	private static final String MODEL_VERSION;
+	static {
+		// Extracting namespace from AmaltheaPackage
+		String nsURI = AmaltheaPackage.eNS_URI;
+		
+		// Extracting AMALTHEA metamodel version
+		MODEL_VERSION = nsURI.lastIndexOf('/') != -1
+				? nsURI.substring(nsURI.lastIndexOf('/') + 1)
+				: nsURI;
+
+	}
+	
     private final String defaultBaseDir = System.getProperty("java.io.tmpdir");
 	
 	private ExecutorService executor = Executors.newFixedThreadPool(1);
@@ -244,6 +259,7 @@
     }
     
     // GET /app4mc/validation/profiles
+    // GET /app4mc/validation/config
     // GET /app4mc/validation/{id}
     // GET /app4mc/validation/{id}/download
     // GET /app4mc/validation/{id}/error
@@ -276,6 +292,47 @@
             }
             
     		return;
+    	} else if (splitPath.length == 2 && "config".equals(splitPath[1])) {
+    		response.setContentType("application/json");
+    		ObjectMapper mapper = new ObjectMapper();
+    		
+    		ObjectNode config = mapper.createObjectNode();
+    		config.put("description", "Validate the input model file.");
+    		
+    		ObjectNode input = mapper.createObjectNode();
+    		input.put("type", "amxmi");
+    		input.put("version", MODEL_VERSION);
+    		input.put("archive-supported", true);
+    		config.set("input", input);
+
+    		// the validation produces no output that is consumed by a following service
+
+    		ObjectNode parameter = mapper.createObjectNode();
+    		ObjectNode profiles = mapper.createObjectNode();
+    		profiles.put("name", "Validation Profiles");
+    		profiles.put("description", "The validation profiles that should be executed.\nIf nothing is selected the Amalthea Standard Validations profile is executed.");
+    		profiles.put("cardinality", "multiple");
+    		// the default value to use is the Amalthea Standard Validations profile
+    		profiles.put("value", "Amalthea Standard Validations");
+    		ArrayNode values = profiles.putArray("values");
+    		
+        	List<String> availableProfiles = manager.getRegisteredValidationProfiles().values().stream()
+    			.sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
+    			.map(profile -> profile.getName())
+    			.collect(Collectors.toList());
+        	availableProfiles.forEach(v -> values.add(v));
+    		
+    		parameter.set("profiles", profiles);
+    		config.set("parameter", parameter);
+    	
+    		try {
+    			String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(config);
+    			response.getWriter().write(json);
+    		} catch (JsonProcessingException e) {
+    			response.getWriter().write(mapper.writeValueAsString(e));
+    		}
+
+    		return;
     	} else if (splitPath.length == 2) {
     		response.setHeader("Cache-Control", "private, no-store, no-cache, must-revalidate");