Added support for multiple results and service configuration file
Change-Id: I60dc72fa671f38dd22bd5b5341362af535b5cfb3
Signed-off-by: Fauth Dirk <Dirk.Fauth@de.bosch.com>
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowController.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowController.java
index 201c8ac..21c7d6e 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowController.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowController.java
@@ -15,6 +15,8 @@
import java.io.File;
import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -99,6 +101,17 @@
return "workflow";
}
+ @PostMapping("/remove/{selected}")
+ public String removeService(
+ @PathVariable(name = "selected") String selected,
+ @ModelAttribute WorkflowStatus ws) {
+
+ ws.removeSelectedService(selected);
+
+ // render the form view
+ return "workflow";
+ }
+
@GetMapping("/selectedServices")
public String getSelectedServices() {
// render the servicesList fragment contained in selectedServices.html
@@ -208,7 +221,7 @@
if (StringUtils.isEmpty(statusUrl)) {
// fallback check for Link header if Location header is not set
statusUrl = getUrlFromLink(uploadResponse.getHeaders().get("Link"), "status", baseUrl);
- } else if (!statusUrl.startsWith(baseUrl)) {
+ } else if (!isValid(statusUrl, baseUrl)) {
throw new ProcessingFailedException(
"The status URL from the Location header does not match with the service base URL '" + baseUrl + "': " + statusUrl);
}
@@ -246,58 +259,78 @@
end = System.currentTimeMillis();
- // don't request more than 30 seconds
+ // don't request for more than 30 seconds
if (end - start > 30_000) {
break;
}
}
// first check if there is a result link
- String downloadUrl = getUrlFromLink(linkHeaders, "result", baseUrl);
+ List<String> resultUrls = getUrlsFromLink(linkHeaders, "result", baseUrl);
+
String deleteUrl = null;
- if (downloadUrl != null) {
+ if (!resultUrls.isEmpty()) {
workflowStatus.addMessage(serviceName + " processing finished");
- // download file
- Path migrationSubDir = storageService.load(workflowStatus.getUuid(), "_" + serviceName.toLowerCase());
- Files.createDirectories(migrationSubDir);
- HttpResponse<File> downloadResponse = Unirest.get(downloadUrl)
- .asFile(migrationSubDir.resolve("result").toString());
+ boolean error = false;
+ for (String resultUrl : resultUrls) {
+
+ // TODO check if link or download
+
+ // download file
+ Path migrationSubDir = storageService.load(workflowStatus.getUuid(), "_" + serviceName.toLowerCase());
+ Files.createDirectories(migrationSubDir);
-
- Path migrationResult = downloadResponse.getBody().toPath();
- String filename = getFilenameFromHeader(downloadResponse.getHeaders().getFirst("Content-Disposition"));
- if (filename != null) {
- migrationResult = Files.move(migrationResult, migrationSubDir.resolve(filename));
+ HttpResponse<File> resultResponse = Unirest.get(resultUrl)
+ .asFile(migrationSubDir.resolve("result").toString());
+
+ // TODO check http status 302
+ // check the response result code
+ if (resultResponse.getStatus() == HttpStatus.SC_OK) {
+
+ Path migrationResult = resultResponse.getBody().toPath();
+ String filename = getFilenameFromHeader(resultResponse.getHeaders().getFirst("Content-Disposition"));
+ if (filename != null) {
+ migrationResult = Files.move(migrationResult, migrationSubDir.resolve(filename));
+ }
+
+ String resultName = resultResponse.getHeaders().getFirst("x-app4mc-result-name");
+ if (StringUtils.isEmpty(resultName)) {
+ resultName = serviceName;
+ }
+
+ workflowStatus.addMessage(resultName + " result downloaded");
+
+ workflowStatus.addResult(
+ resultName + " Result",
+ MvcUriComponentsBuilder.fromMethodName(
+ WorkflowController.class,
+ "serveFile",
+ workflowStatus.getUuid(),
+ "_" + serviceName.toLowerCase(),
+ migrationResult.getFileName().toString()).build().toUri().toString());
+
+ // extract header information if result should be used in workflow
+ String useResult = resultResponse.getHeaders().getFirst("x-app4mc-use-result");
+ if (useResult == null || !useResult.toLowerCase().equals("false")) {
+ // the result should be used in the workflow
+ result = migrationResult;
+ }
+ } else {
+ error = true;
+ }
+
+ if (deleteUrl == null) {
+ // extract delete
+ deleteUrl = getUrlFromLink(resultResponse.getHeaders().get("Link"), "delete", baseUrl);
+ }
}
- workflowStatus.addMessage(serviceName + " result downloaded");
-
- workflowStatus.addResult(
- serviceName + " Result",
- MvcUriComponentsBuilder.fromMethodName(
- WorkflowController.class,
- "serveFile",
- workflowStatus.getUuid(),
- "_" + serviceName.toLowerCase(),
- migrationResult.getFileName().toString()).build().toUri().toString());
-
- // extract header information if result should be used in workflow
- String useResult = downloadResponse.getHeaders().getFirst("x-app4mc-use-result");
- if (useResult == null || !useResult.toLowerCase().equals("false")) {
- // the result should not be used in the workflow
- result = migrationResult;
- }
-
- // check the response result code
- if (downloadResponse.getStatus() == HttpStatus.SC_OK) {
- workflowStatus.addMessage(serviceName + " successfull");
+ if (error) {
+ workflowStatus.addError(serviceName + " failed with errors");
} else {
- workflowStatus.addError(serviceName + " failed with errors");
+ workflowStatus.addMessage(serviceName + " successfull");
}
-
- // extract delete
- deleteUrl = getUrlFromLink(downloadResponse.getHeaders().get("Link"), "delete", baseUrl);
} else {
String errorUrl = getUrlFromLink(linkHeaders, "error", baseUrl);
if (errorUrl != null) {
@@ -329,6 +362,7 @@
}
}
+ // TODO add check for delete configuration parameter
if (deleteUrl != null) {
// delete upload again
Unirest.delete(deleteUrl).asEmpty();
@@ -344,7 +378,7 @@
return result;
} catch (StorageException | IOException e) {
- throw new ProcessingFailedException("Error on file operations in converter workflow", e);
+ throw new ProcessingFailedException("Error on file operations in " + serviceName + " workflow", e);
}
}
@@ -409,7 +443,7 @@
if (rel.equals(relValue)) {
// SECURITY: ensure that host in link matches host in configured service
- if (!url.startsWith(baseUrl)) {
+ if (!isValid(url, baseUrl)) {
throw new ProcessingFailedException(
"The link for rel '" + rel + "' does not match with the service base URL '" + baseUrl + "': " + url);
}
@@ -424,6 +458,69 @@
}
/**
+ * Extracts the url from the link header and returns the url that has the
+ * specified rel attribute set.
+ *
+ * @param linkHeaders The link headers to parse.
+ * @param rel The value for the rel param for which the url is
+ * requested.
+ * @param baseUrl The base URL of the service.
+ * @return The url for the specified rel param or <code>null</code> if there is
+ * no link for the given rel.
+ */
+ public static List<String> getUrlsFromLink(List<String> linkHeaders, String rel, String baseUrl) {
+
+ ArrayList<String> results = new ArrayList<>();
+ ArrayList<String> errors = new ArrayList<>();
+
+ for (String linkHeader : linkHeaders) {
+ String[] links = linkHeader.split(LINK_DELIMITER);
+
+ for (String link : links) {
+ String[] segments = link.split(LINK_PARAM_DELIMITER);
+
+ if (segments.length < 2) {
+ continue;
+ }
+
+ String url = segments[0].trim();
+ if (!url.startsWith("<") || !url.endsWith(">")) {
+ continue;
+ }
+ url = url.substring(1, url.length() - 1);
+
+ for (int i = 1; i < segments.length; i++) {
+ String[] param = segments[i].trim().split("=");
+ if (param.length < 2 || !"rel".equals(param[0])) {
+ continue;
+ }
+
+ String relValue = param[1];
+ if (relValue.startsWith("\"") && relValue.endsWith("\"")) {
+ relValue = relValue.substring(1, relValue.length() - 1);
+ }
+
+ if (rel.equals(relValue)) {
+ // SECURITY: ensure that host in link matches host in configured service
+ if (isValid(url, baseUrl)) {
+ results.add(url);
+ } else {
+ errors.add("The link for rel '" + rel + "' does not match with the service base URL '" + baseUrl + "': " + url);
+ }
+ }
+ }
+ }
+ }
+
+ if (results.isEmpty() && !errors.isEmpty()) {
+ String error = String.join("\n", errors);
+ throw new ProcessingFailedException(error);
+ }
+
+ return results;
+ }
+
+ /**
* Extracts the filename from the Content-Disposition header.
*
* @param header The Content-Disposition header to parse.
@@ -444,4 +541,41 @@
return null;
}
+ /**
+ * Verify if the URL to check has the same host as the given base URL. This
+ * method is used to avoid that a service result redirects to another server.
+ *
+ * @param toCheck The URL String that should be checked.
+ * @param base The base URL String to check against.
+ * @return <code>true</code> if the host parts of the URLs are equal,
+ * <code>false</code> in any other case.
+ */
+ protected static boolean isValid(String toCheck, String base) {
+ if (StringUtils.isEmpty(toCheck) || StringUtils.isEmpty(base)) {
+ throw new ProcessingFailedException("URLs to compare can not be empty");
+ }
+
+ URL toCheckUrl = null;
+ try {
+ toCheckUrl = new URL(toCheck);
+ } catch (MalformedURLException e) {
+ throw new ProcessingFailedException("URL to check is not in a valid format", e);
+ }
+
+ URL baseUrl = null;
+ try {
+ baseUrl = new URL(base);
+ } catch (MalformedURLException e) {
+ throw new ProcessingFailedException("URL to check against is not in a valid format", e);
+ }
+
+ if (baseUrl != null && toCheckUrl != null) {
+ // TODO clarify: check only host, or host:port, or host:port/path
+// return baseUrl.getAuthority().equals(toCheckUrl.getAuthority());
+// return baseUrl.getAuthority().equals(toCheckUrl.getAuthority());
+ return toCheckUrl.getAuthority().equals(baseUrl.getAuthority()) && toCheckUrl.getPath().startsWith(baseUrl.getPath());
+ }
+
+ return false;
+ }
}
\ No newline at end of file
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatus.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatus.java
index 0fb9b85..1e7eae3 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatus.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatus.java
@@ -41,6 +41,10 @@
this.selectedServices.add(service);
}
+ public void removeSelectedService(String service) {
+ this.selectedServices.remove(service);
+ }
+
public ArrayList<String> getMessages() {
return messages;
}
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 a6ae0b8..ef8f86b 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
@@ -13,11 +13,18 @@
*/
package org.eclipse.app4mc.cloud.manager.administration;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.util.ResourceUtils;
import org.springframework.web.context.annotation.ApplicationScope;
@Configuration
@@ -26,9 +33,30 @@
@Bean
@ApplicationScope
public List<CloudServiceDefinition> cloudServiceDefinitions() {
- ArrayList<CloudServiceDefinition> definitions = new ArrayList<>();
- definitions.add(new CloudServiceDefinition("Migration", "http://localhost:8080/app4mc/converter/", "Model Migration Service"));
- definitions.add(new CloudServiceDefinition("Validation", "http://localhost:8181/app4mc/validation/", "Model Validation Service"));
+ ArrayList<CloudServiceDefinition> definitions = new ArrayList<>();
+
+ String config = System.getProperty("service.configuration");
+ if (config != null) {
+ try {
+ File file = ResourceUtils.getFile(config);
+ Path path = Paths.get(file.toURI());
+ List<CloudServiceDefinition> services = Files.lines(path).map(line -> {
+ String[] split = line.split(";");
+ String name = split[0];
+ String url = split[1];
+ String desc = split.length >= 3 ? split[2] : "";
+ return new CloudServiceDefinition(name, url, desc);
+ }).collect(Collectors.toList());
+ definitions.addAll(services);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (definitions.isEmpty()) {
+ definitions.add(new CloudServiceDefinition("Migration", "http://localhost:8080/app4mc/converter/", "Model Migration Service"));
+ definitions.add(new CloudServiceDefinition("Validation", "http://localhost:8181/app4mc/validation/", "Model Validation Service"));
+ }
return definitions;
}
diff --git a/manager/src/main/resources/application.properties b/manager/src/main/resources/application.properties
index 51ad5eb..52ed629 100644
--- a/manager/src/main/resources/application.properties
+++ b/manager/src/main/resources/application.properties
@@ -1 +1,2 @@
-server.port=9090
+spring.servlet.multipart.max-file-size=20MB
+spring.servlet.multipart.max-request-size=20MB
diff --git a/manager/src/main/resources/services.txt b/manager/src/main/resources/services.txt
new file mode 100644
index 0000000..9789c18
--- /dev/null
+++ b/manager/src/main/resources/services.txt
@@ -0,0 +1,6 @@
+Migration;http://localhost:8080/app4mc/converter/;Model Migration Service
+Validation;http://localhost:8181/app4mc/validation/;Model Validation Service
+INCHRON;https://am2inc.dev1.inchron.de/projects;INCHRON Transformation Service
+RTC Analysis;http://localhost:8081/app4mc/analysis/;RTC Analysis Service
+Chart Interpreter;http://localhost:8082/rtc/nodes/;Chart Interpreter Service
+Chart Visualizer;http://localhost:8083/charts/barchart/;Chart Visualizer Service
\ No newline at end of file
diff --git a/manager/src/main/resources/templates/selectedServices.html b/manager/src/main/resources/templates/selectedServices.html
index 904f5f0..3709203 100644
--- a/manager/src/main/resources/templates/selectedServices.html
+++ b/manager/src/main/resources/templates/selectedServices.html
@@ -7,15 +7,9 @@
<body>
<div th:fragment="servicesList" id="selectedServices">
<span th:each="selected : ${workflowStatus.selectedServices}">
- <select name="services" id="services" onchange="updateSelectedServices(this.value)">
- <option></option>
- <option
- th:each="service : ${cloudServiceDefinitions}"
- th:text="${service.name}"
- th:value="${service.name}"
- th:selected="${service.name == selected}">
- </option>
- </select><br>
+ <span th:text="${selected}">Service</span>
+ <button type="button" class="btn btn-default" th:attr="onclick=|removeSelectedServices('${selected}')|" >remove</button>
+ <br>
</span>
<select name="services" id="services" onchange="updateSelectedServices(this.value)">
<option></option>
diff --git a/manager/src/main/resources/templates/workflow.html b/manager/src/main/resources/templates/workflow.html
index 48d1c34..8268cde 100644
--- a/manager/src/main/resources/templates/workflow.html
+++ b/manager/src/main/resources/templates/workflow.html
@@ -14,6 +14,16 @@
});
}
+function removeSelectedServices(service) {
+ $.ajax({
+ type: 'POST',
+ url: '/remove/' + service,
+ success: function(result) {
+ $('#selectedServicesBlock').load('/selectedServices');
+ }
+ });
+}
+
$(document).ready(function(){
$('#selectedServicesBlock').load('/selectedServices');
});
@@ -33,8 +43,8 @@
</div>
</td>
</tr>
- <tr>
- <td>Select validations to perform:</td>
+ <tr th:if="${allProfiles != null and not allProfiles.isEmpty()}">
+ <td valign="top">Select validations to perform:</td>
<td>
<ul>
<li th:each="profile : ${allProfiles}">
diff --git a/manager/src/test/java/org/eclipse/app4mc/cloud/manager/WorkflowControllerTest.java b/manager/src/test/java/org/eclipse/app4mc/cloud/manager/WorkflowControllerTest.java
new file mode 100644
index 0000000..7938d9b
--- /dev/null
+++ b/manager/src/test/java/org/eclipse/app4mc/cloud/manager/WorkflowControllerTest.java
@@ -0,0 +1,143 @@
+/*********************************************************************************
+ * Copyright (c) 2020 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+class WorkflowControllerTest {
+
+ @Test
+ void shouldCheckValidUrls() {
+ String baseUrl = "http://localhost:8080/app4mc/converter";
+ String toCheckUrl = "http://localhost:8080/app4mc/converter/5240471251956758767";
+
+ assertTrue(WorkflowController.isValid(toCheckUrl, baseUrl));
+ }
+
+ @Test
+ void shouldCheckValidUrlsDifferentProtocols() {
+ String baseUrl = "http://localhost:8080/app4mc/converter";
+ String toCheckUrl = "https://localhost:8080/app4mc/converter/5240471251956758767";
+
+ assertTrue(WorkflowController.isValid(toCheckUrl, baseUrl));
+ }
+
+ @Test
+ void shouldFireExceptionOnEmptyParameter() {
+ String baseUrl = "http://localhost:8080/app4mc/converter";
+ String toCheckUrl = "https://localhost:8080/app4mc/converter/5240471251956758767";
+
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.isValid(null, baseUrl);
+ });
+
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.isValid(toCheckUrl, null);
+ });
+
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.isValid(null, null);
+ });
+
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.isValid("", baseUrl);
+ });
+
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.isValid(toCheckUrl, "");
+ });
+
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.isValid("", "");
+ });
+ }
+
+ @Test
+ void shouldFireExceptionOnInvalidUrl() {
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.isValid("https://localhost:8080/app4mc/converter/5240471251956758767", "localhost:8080/app4mc/converter");
+ });
+
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.isValid("localhost:8080/app4mc/converter/5240471251956758767", "https://localhost:8080/app4mc/converter");
+ });
+ }
+
+ @Test
+ void shouldGetResultUrlFromLinkHeader() {
+ List<String> linkHeaders = Arrays.asList(
+ "<http://localhost:8080/app4mc/converter/5240471251956758767>;rel=\"self\"",
+ "<http://localhost:8080/app4mc/converter/5240471251956758767/download>;rel=\"result\"");
+
+ String result = WorkflowController.getUrlFromLink(linkHeaders, "result", "http://localhost:8080/app4mc/converter");
+
+ assertEquals("http://localhost:8080/app4mc/converter/5240471251956758767/download", result);
+ }
+
+ @Test
+ void shouldReturnNullForNonExistingRel() {
+ List<String> linkHeaders = Arrays.asList(
+ "<http://localhost:8080/app4mc/converter/5240471251956758767>;rel=\"self\"",
+ "<http://localhost:8080/app4mc/converter/5240471251956758767/download>;rel=\"result\"");
+
+ String result = WorkflowController.getUrlFromLink(linkHeaders, "delete", "http://localhost:8080/app4mc/converter");
+
+ assertNull(result);
+ }
+
+ @Test
+ void shouldReturnMultipleResults() {
+ List<String> linkHeaders = Arrays.asList(
+ "<http://localhost:8080/app4mc/converter/5240471251956758767/simple>;rel=\"result\"",
+ "<http://localhost:8080/app4mc/converter/5240471251956758767/download>;rel=\"result\"");
+
+ List<String> urls = WorkflowController.getUrlsFromLink(linkHeaders, "result", "http://localhost:8080/app4mc/converter");
+
+ assertEquals(2, urls.size());
+ assertTrue(urls.contains("http://localhost:8080/app4mc/converter/5240471251956758767/simple"));
+ assertTrue(urls.contains("http://localhost:8080/app4mc/converter/5240471251956758767/download"));
+ }
+
+ @Test
+ void shouldOnlyReturnValidResult() {
+ List<String> linkHeaders = Arrays.asList(
+ "<http://localhost:8080/app4mc/converter/5240471251956758767/simple>;rel=\"result\"",
+ "<http://www.google.com:8080/app4mc/converter/5240471251956758767/download>;rel=\"result\"");
+
+ List<String> urls = WorkflowController.getUrlsFromLink(linkHeaders, "result", "http://localhost:8080/app4mc/converter");
+
+ assertEquals(1, urls.size());
+ assertTrue(urls.contains("http://localhost:8080/app4mc/converter/5240471251956758767/simple"));
+ }
+
+ @Test
+ void shouldThrowExceptionForNonEqualHosts() {
+ List<String> linkHeaders = Arrays.asList(
+ "<http://www.amazon.com/app4mc/converter/5240471251956758767>;rel=\"result\"",
+ "<http://www.google.com:8080/app4mc/converter/5240471251956758767/download>;rel=\"result\"");
+
+ assertThrows(ProcessingFailedException.class, () -> {
+ WorkflowController.getUrlsFromLink(linkHeaders, "result", "http://localhost:8080/app4mc/converter");
+ });
+
+ }
+}