Updated REST API design
Change-Id: Ic83bca61dcebfe8daf930f1ff109450419047a12
Signed-off-by: Fauth Dirk <Dirk.Fauth@de.bosch.com>
diff --git a/docker-clear.bat b/docker-clear.bat
new file mode 100644
index 0000000..9027a47
--- /dev/null
+++ b/docker-clear.bat
@@ -0,0 +1,3 @@
+@echo off
+FOR /f "tokens=*" %%i IN ('docker ps -aq') DO docker rm %%i
+FOR /f "tokens=*" %%i IN ('docker images --format "{{.ID}}"') DO docker rmi %%i
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c214311
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,23 @@
+version: "3.8"
+services:
+ converter:
+ build:
+ context: ./org.eclipse.app4mc.converter.cloud
+ container_name: app4mc_cloud_converter
+ ports:
+ - "8080:8080"
+
+ validation:
+ build:
+ context: ./org.eclipse.app4mc.validation.cloud
+ container_name: app4mc_cloud_validation
+ ports:
+ - "8181:8181"
+
+ manager:
+ build:
+ context: ./manager
+ container_name: app4mc_cloud_manager
+ ports:
+ - "9090:9090"
+
\ No newline at end of file
diff --git a/manager/Dockerfile b/manager/Dockerfile
new file mode 100644
index 0000000..c980683
--- /dev/null
+++ b/manager/Dockerfile
@@ -0,0 +1,5 @@
+FROM amazoncorretto:8u265
+
+COPY target/manager-0.0.1-SNAPSHOT.jar app.jar
+
+ENTRYPOINT ["java","-jar","/app.jar"]
\ No newline at end of file
diff --git a/manager/pom.xml b/manager/pom.xml
index f426dcb..551fab8 100644
--- a/manager/pom.xml
+++ b/manager/pom.xml
@@ -11,7 +11,6 @@
<groupId>org.eclipse.app4mc.cloud</groupId>
<artifactId>manager</artifactId>
<version>0.0.1-SNAPSHOT</version>
- <packaging>war</packaging>
<name>APP4MC Cloud Manager</name>
<description>Manager application to orchestrate AMALTHEA model processing in the cloud</description>
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 f6a257c..e1fcd9e 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
@@ -13,11 +13,15 @@
*/
package org.eclipse.app4mc.cloud.manager;
+import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.http.HttpStatus;
+import org.eclipse.app4mc.cloud.manager.administration.CloudServiceDefinition;
import org.eclipse.app4mc.cloud.manager.storage.StorageException;
import org.eclipse.app4mc.cloud.manager.storage.StorageFileNotFoundException;
import org.eclipse.app4mc.cloud.manager.storage.StorageService;
@@ -47,10 +51,9 @@
private final StorageService storageService;
- // TODO make url configurable
- private String CONVERTER_SERVICE_BASE_URL = "http://localhost:8080/app4mc/converter/";
- private String VALIDATION_SERVICE_BASE_URL = "http://localhost:8181/app4mc/validation/";
-
+ @javax.annotation.Resource(name = "cloudServiceDefinitions")
+ List<CloudServiceDefinition> cloudServiceDefinitions;
+
@Autowired
public WorkflowController(StorageService storageService) {
this.storageService = storageService;
@@ -59,7 +62,26 @@
@GetMapping("/workflow")
public String workflow(Model model) {
- model.addAttribute("allProfiles", Unirest.get(VALIDATION_SERVICE_BASE_URL + "profiles").asJson().getBody().getArray().toList());
+ // TODO move this to a configuration resource mechanism
+ CloudServiceDefinition csd = this.cloudServiceDefinitions.stream()
+ .filter(sd -> sd.getName().equals("Validation"))
+ .findFirst()
+ .orElse(null);
+
+ List<?> allProfiles = new ArrayList<>();
+ if (csd != null) {
+ try {
+ String baseUrl = csd.getBaseUrl();
+ if (!baseUrl.endsWith("/")) {
+ baseUrl += "/";
+ }
+ allProfiles = Unirest.get(baseUrl + "profiles").asJson().getBody().getArray().toList();
+ } catch (Exception e) {
+ // do nothing, we will handle configurations in a different way in the future
+ }
+ }
+
+ model.addAttribute("allProfiles", allProfiles);
// render the form view
return "workflow";
@@ -68,8 +90,8 @@
@PostMapping("/workflow")
public String handleFileUpload(
@RequestParam("file") MultipartFile file,
- @RequestParam("services") String[] services,
- @RequestParam("profiles") String[] validationProfiles,
+ @RequestParam(name = "services", required = false) String[] services,
+ @RequestParam(name = "profiles", required = false) String[] validationProfiles,
Model model,
@ModelAttribute WorkflowStatus ws) {
@@ -92,21 +114,20 @@
workflowStatus.addMessage(file.getOriginalFilename() + " successfully uploaded!");
model.addAttribute("workflowStatus", workflowStatus);
- // TODO add proper error handling
- Path uploaded = storageService.load(uuid, file.getOriginalFilename());
- try {
- Path inputFile = uploaded;
-
- if (Arrays.stream(services).anyMatch(s -> "Migration".equals(s))) {
- inputFile = executeConversion(workflowStatus, inputFile, file.getOriginalFilename());
- }
-
- if (Arrays.stream(services).anyMatch(s -> "Validation".equals(s))) {
- executeValidation(workflowStatus, inputFile, file.getOriginalFilename(), validationProfiles);
- }
+ if (services != null) {
+ Path inputFile = storageService.load(uuid, file.getOriginalFilename());
+ for (String service : services) {
+ // if an error occurred stop the workflow
+ if (!workflowStatus.getErrors().isEmpty()) {
+ break;
+ }
- } catch (ProcessingFailedException e) {
- workflowStatus.addError(e.getMessage());
+ try {
+ inputFile = executeCloudService(service, workflowStatus, inputFile, file.getOriginalFilename());
+ } catch (ProcessingFailedException e) {
+ workflowStatus.addError(e.getMessage());
+ }
+ }
}
return "redirect:/workflow";
@@ -138,99 +159,166 @@
return "redirect:/workflow";
}
- private Path executeConversion(WorkflowStatus workflowStatus, Path inputFile, String originalFilename) {
+ private Path executeCloudService(String serviceName, WorkflowStatus workflowStatus, Path inputFile, String originalFilename) {
try {
- // upload to converter service
- String converterUuid = Unirest.post(CONVERTER_SERVICE_BASE_URL + "upload")
+ CloudServiceDefinition csd = this.cloudServiceDefinitions.stream()
+ .filter(sd -> sd.getName().equals(serviceName))
+ .findFirst()
+ .orElse(null);
+
+ String baseUrl = "";
+ if (csd != null) {
+ baseUrl = csd.getBaseUrl();
+ } else {
+ workflowStatus.addError("No service with name " + serviceName + " found! Workflow stopped!");
+ return null;
+ }
+
+ // upload to service
+ HttpResponse<?> uploadResponse = Unirest.post(baseUrl)
.field("file", Files.newInputStream(inputFile), originalFilename)
- .asString()
- .getBody();
+ .asEmpty();
- workflowStatus.addMessage("Upload to migration service succeeded");
+ // extract status link from result
+ String statusUrl = null;
+ if (uploadResponse.getStatus() == 201) {
+ statusUrl = uploadResponse.getHeaders().getFirst("Location");
+ } else if (uploadResponse.getStatus() == 200) {
+ // fallback if return code is 200, then follow up needs to be placed in Link header
+ statusUrl = getUrlFromLink(uploadResponse.getHeaders().get("Link"), "status");
+ } else {
+ // error
+ workflowStatus.addError("Upload to " + serviceName + " failed! Workflow stopped!");
+ return null;
+ }
- // trigger conversion
- Unirest.put(CONVERTER_SERVICE_BASE_URL + converterUuid + "/convert").asEmpty();
+ workflowStatus.addMessage("Upload to " + serviceName + " service succeeded");
- workflowStatus.addMessage("Migration done");
+ Path result = inputFile;
- // download file
- Path migrationSubDir = storageService.load(workflowStatus.getUuid(), "_migration");
- Files.createDirectories(migrationSubDir);
- Path migrationResult = Unirest.get(CONVERTER_SERVICE_BASE_URL + converterUuid + "/download")
- .asFile(migrationSubDir.resolve(originalFilename).toString())
- .getBody()
- .toPath();
+ // trigger status URL until process is finished or error occured
+ if (statusUrl != null) {
+ HttpResponse<?> statusResponse = Unirest.get(statusUrl).asEmpty();
+ List<String> linkHeaders = statusResponse.getHeaders().get("Link");
+
+ // TODO make this asynchronous using websockets
+ long start = System.currentTimeMillis();
+ long end = System.currentTimeMillis();
+ while (linkHeaders.size() <= 1) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ statusResponse = Unirest.get(statusUrl).asEmpty();
+ linkHeaders = statusResponse.getHeaders().get("Link");
+
+ end = System.currentTimeMillis();
+
+ // don't request more than 30 seconds
+ if (end - start > 30_000) {
+ break;
+ }
+ }
+
+ // first check if there is a result link
+ String downloadUrl = getUrlFromLink(linkHeaders, "result");
+ String deleteUrl = null;
+ if (downloadUrl != null) {
+ 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());
+
+
+ Path migrationResult = downloadResponse.getBody().toPath();
+ String filename = getFilenameFromHeader(downloadResponse.getHeaders().getFirst("Content-Disposition"));
+ if (filename != null) {
+ migrationResult = Files.move(migrationResult, migrationSubDir.resolve(filename));
+ }
+
+ 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");
+ } else {
+ workflowStatus.addError(serviceName + " failed with errors");
+ }
+
+ // extract delete
+ deleteUrl = getUrlFromLink(downloadResponse.getHeaders().get("Link"), "delete");
+ } else {
+ String errorUrl = getUrlFromLink(linkHeaders, "error");
+ if (errorUrl != null) {
+ workflowStatus.addMessage(serviceName + " processing finished with error");
+
+ // download error file
+ Path migrationSubDir = storageService.load(workflowStatus.getUuid(), "_" + serviceName.toLowerCase());
+ Files.createDirectories(migrationSubDir);
+ HttpResponse<File> errorResponse = Unirest.get(errorUrl)
+ .asFile(migrationSubDir.resolve("error").toString());
+
+ Path migrationError = errorResponse.getBody().toPath();
+ String filename = getFilenameFromHeader(errorResponse.getHeaders().getFirst("Content-Disposition"));
+ migrationError = Files.move(migrationError, migrationSubDir.resolve(filename));
+
+ workflowStatus.addMessage(serviceName + " error result downloaded");
+
+ workflowStatus.addResult(
+ serviceName + " Error",
+ MvcUriComponentsBuilder.fromMethodName(
+ WorkflowController.class,
+ "serveFile",
+ workflowStatus.getUuid(),
+ "_" + serviceName.toLowerCase(),
+ migrationError.getFileName().toString()).build().toUri().toString());
+
+ // extract delete
+ deleteUrl = getUrlFromLink(errorResponse.getHeaders().get("Link"), "delete");
+ }
+ }
+
+ if (deleteUrl != null) {
+ // delete upload again
+ Unirest.delete(deleteUrl).asEmpty();
+ workflowStatus.addMessage(serviceName + " cleaned up");
+ }
+
+ } else {
+ // no status URL in the upload response, stop further processing
+ workflowStatus.addMessage("No status URL found for service " + serviceName);
+ return inputFile;
+ }
- workflowStatus.addMessage("Migrated file downloaded");
- workflowStatus.addResult(
- "Migration Result",
- MvcUriComponentsBuilder.fromMethodName(
- WorkflowController.class,
- "serveFile",
- workflowStatus.getUuid(),
- "_migration",
- migrationResult.getFileName().toString()).build().toUri().toString());
-
- // delete converter upload again
- Unirest.delete(CONVERTER_SERVICE_BASE_URL + converterUuid + "/delete").asEmpty();
-
- workflowStatus.addMessage("Migration service cleaned up");
-
- return migrationResult;
+ return result;
} catch (StorageException | IOException e) {
throw new ProcessingFailedException("Error on file operations in converter workflow", e);
}
}
- private void executeValidation(WorkflowStatus workflowStatus, Path inputFile, String originalFilename, String[] validationProfiles) {
- try {
- // upload to validation service
- String validationUuid = Unirest.post(VALIDATION_SERVICE_BASE_URL + "upload")
- .field("file", Files.newInputStream(inputFile), originalFilename)
- .field("profiles", Arrays.asList(validationProfiles))
- .asString()
- .getBody();
-
- workflowStatus.addMessage("Upload to validation service succeeded");
-
- // trigger conversion
- HttpResponse<?> httpResponse = Unirest.put(VALIDATION_SERVICE_BASE_URL + validationUuid + "/validate").asEmpty();
-
- // download file
- Path validationSubDir = storageService.load(workflowStatus.getUuid(), "_validation");
- Files.createDirectories(validationSubDir);
- Path validationResult = Unirest.get(VALIDATION_SERVICE_BASE_URL + validationUuid + "/download")
- .asFile(validationSubDir.resolve("validation_result.txt").toString())
- .getBody()
- .toPath();
-
- workflowStatus.addMessage("Validation result downloaded");
- workflowStatus.addResult(
- "Validation Result",
- MvcUriComponentsBuilder.fromMethodName(
- WorkflowController.class,
- "serveFile",
- workflowStatus.getUuid(),
- "_validation",
- validationResult.getFileName().toString()).build().toUri().toString());
-
- // delete validation upload again
- Unirest.delete(VALIDATION_SERVICE_BASE_URL + validationUuid + "/delete").asEmpty();
-
- workflowStatus.addMessage("Validation service cleaned up");
-
- if (httpResponse.getStatus() == 200) {
- workflowStatus.addMessage("Validation successfull");
- } else {
- throw new ProcessingFailedException("Validation failed with errors");
- }
-
- } catch (StorageException | IOException e) {
- throw new ProcessingFailedException("Error on file operations in validation workflow", e);
- }
- }
-
@ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
@@ -240,4 +328,84 @@
public WorkflowStatus workflowStatus() {
return new WorkflowStatus();
}
+
+ @ModelAttribute("cloudServiceDefinitions")
+ public List<CloudServiceDefinition> cloudServiceDefinitions() {
+ return cloudServiceDefinitions;
+ }
+
+
+ private static final String LINK_DELIMITER = ",";
+ private static final String LINK_PARAM_DELIMITER = ";";
+
+ /**
+ * 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.
+ * @return The url for the specified rel param or <code>null</code> if there is
+ * no link for the given rel.
+ */
+ public static String getUrlFromLink(List<String> linkHeaders, String rel) {
+
+ 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)) {
+ return url;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts the filename from the Content-Disposition header.
+ *
+ * @param header The Content-Disposition header to parse.
+ * @return The filename from the header or <code>null</code> if no filename
+ * could be extracted.
+ */
+ public static String getFilenameFromHeader(String header) {
+ String[] segments = header.split(LINK_PARAM_DELIMITER);
+ for (String segment : segments) {
+ if (segment.trim().startsWith("filename=")) {
+ String filename = segment.substring(segment.indexOf('=') + 1);
+ if (filename.startsWith("\"") && filename.endsWith("\"")) {
+ filename = filename.substring(1, filename.length() - 1);
+ }
+ return filename;
+ }
+ }
+ return null;
+ }
+
}
\ No newline at end of file
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/AdminController.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/AdminController.java
new file mode 100644
index 0000000..c86570c
--- /dev/null
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/AdminController.java
@@ -0,0 +1,60 @@
+/*********************************************************************************
+ * 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.administration;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Resource;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@Controller
+public class AdminController {
+
+ @Resource(name = "cloudServiceDefinitions")
+ List<CloudServiceDefinition> cloudServiceDefinitions;
+
+ @GetMapping("/admin")
+ public String adminRequest(final Model model) {
+ CloudServiceDefinitionDTO dto = new CloudServiceDefinitionDTO(cloudServiceDefinitions);
+ dto.addService(new CloudServiceDefinition());
+
+ model.addAttribute("dto", dto);
+
+ return "admin";
+ }
+
+ @PostMapping("/admin/save")
+ public String saveBooks(@ModelAttribute CloudServiceDefinitionDTO dto, Model model) {
+ // TODO save to DB
+ cloudServiceDefinitions.clear();
+ cloudServiceDefinitions.addAll(dto.getServices().stream()
+ .filter(csd -> !StringUtils.isEmpty(csd.getName()) && !StringUtils.isEmpty(csd.getBaseUrl()))
+ .collect(Collectors.toList()));
+
+ model.addAttribute("dto", new CloudServiceDefinitionDTO(cloudServiceDefinitions));
+ return "redirect:/admin";
+ }
+
+ @ModelAttribute("cloudServiceDefinitions")
+ public List<CloudServiceDefinition> cloudServiceDefinitions() {
+ return cloudServiceDefinitions;
+ }
+}
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
new file mode 100644
index 0000000..c69243a
--- /dev/null
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/ApplicationConfig.java
@@ -0,0 +1,21 @@
+package org.eclipse.app4mc.cloud.manager.administration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.context.annotation.ApplicationScope;
+
+@Configuration
+public class ApplicationConfig {
+
+ @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"));
+ return definitions;
+ }
+}
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
new file mode 100644
index 0000000..f1069bc
--- /dev/null
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/CloudServiceDefinition.java
@@ -0,0 +1,43 @@
+package org.eclipse.app4mc.cloud.manager.administration;
+
+public class CloudServiceDefinition {
+
+ String name;
+ String baseUrl;
+ String description;
+
+ public CloudServiceDefinition() {
+ // empty default constructor
+ }
+
+ public CloudServiceDefinition(String name, String baseUrl, String description) {
+ this.name = name;
+ this.baseUrl = baseUrl;
+ this.description = description;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+}
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/CloudServiceDefinitionDTO.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/CloudServiceDefinitionDTO.java
new file mode 100644
index 0000000..5f71883
--- /dev/null
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/administration/CloudServiceDefinitionDTO.java
@@ -0,0 +1,29 @@
+package org.eclipse.app4mc.cloud.manager.administration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CloudServiceDefinitionDTO {
+
+ private ArrayList<CloudServiceDefinition> services = new ArrayList<>();
+
+ public CloudServiceDefinitionDTO(List<CloudServiceDefinition> services) {
+ if (services != null) {
+ this.services = new ArrayList<CloudServiceDefinition>(services);
+ } else {
+ this.services = new ArrayList<>();
+ }
+ }
+
+ public ArrayList<CloudServiceDefinition> getServices() {
+ return services;
+ }
+
+ public void setServices(ArrayList<CloudServiceDefinition> services) {
+ this.services = services;
+ }
+
+ public void addService(CloudServiceDefinition service) {
+ this.services.add(service);
+ }
+}
diff --git a/manager/src/main/resources/static/index.html b/manager/src/main/resources/static/index.html
index eb1604f..be3776e 100644
--- a/manager/src/main/resources/static/index.html
+++ b/manager/src/main/resources/static/index.html
@@ -7,5 +7,6 @@
<body>
<h1>APP4MC Cloud Manager</h1>
<p><a href="/workflow">Start Workflow</a></p>
+ <p><a href="/admin">Administration</a></p>
</body>
</html>
diff --git a/manager/src/main/resources/templates/admin.html b/manager/src/main/resources/templates/admin.html
new file mode 100644
index 0000000..323059e
--- /dev/null
+++ b/manager/src/main/resources/templates/admin.html
@@ -0,0 +1,37 @@
+<html xmlns:th="https://www.thymeleaf.org">
+<head>
+ <title>APP4MC Cloud Manager - Administration</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+<body>
+
+ <div>
+ <h2>Configure available APP4MC Cloud services</h2>
+ </div>
+ <div>
+ <form action="#" th:action="@{/admin/save}" th:object="${dto}" method="POST">
+ <fieldset>
+ <input type="submit" id="submitButton" th:value="Save">
+ <input type="reset" id="resetButton" name="reset" th:value="Reset"/>
+ <table>
+ <thead>
+ <tr>
+ <th>Service Name</th>
+ <th>Service Base URL</th>
+ <th>Service Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr th:each="service, stat : *{services}">
+ <td><input th:field="*{services[__${stat.index}__].name}" /></td>
+ <td><input th:field="*{services[__${stat.index}__].baseUrl}" /></td>
+ <td><input th:field="*{services[__${stat.index}__].description}" /></td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ </form>
+ </div>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/manager/src/main/resources/templates/workflow.html b/manager/src/main/resources/templates/workflow.html
index b59c211..06c5df3 100644
--- a/manager/src/main/resources/templates/workflow.html
+++ b/manager/src/main/resources/templates/workflow.html
@@ -12,8 +12,9 @@
<tr>
<td>Select service(s) to process:</td>
<td>
- <input type="checkbox" name="services" value="Migration" />Migration<br>
- <input type="checkbox" name="services" value="Validation" />Validation<br>
+ <span th:each="service : ${cloudServiceDefinitions}">
+ <input type="checkbox" name="services" th:text="${service.name}" th:value="${service.name}" /><br>
+ </span>
</td>
</tr>
<tr>
diff --git a/org.eclipse.app4mc.converter.cloud/Dockerfile b/org.eclipse.app4mc.converter.cloud/Dockerfile
new file mode 100644
index 0000000..58ec81d
--- /dev/null
+++ b/org.eclipse.app4mc.converter.cloud/Dockerfile
@@ -0,0 +1,5 @@
+FROM amazoncorretto:8u265
+
+COPY converter-app/target/converter-app.jar converter-app.jar
+
+ENTRYPOINT ["java","-jar","/converter-app.jar"]
\ No newline at end of file
diff --git a/org.eclipse.app4mc.converter.cloud/converter-app/converter-app.bndrun b/org.eclipse.app4mc.converter.cloud/converter-app/converter-app.bndrun
index a92513f..d849fba 100644
--- a/org.eclipse.app4mc.converter.cloud/converter-app/converter-app.bndrun
+++ b/org.eclipse.app4mc.converter.cloud/converter-app/converter-app.bndrun
@@ -19,7 +19,8 @@
bnd.identity;id='org.eclipse.app4mc.amalthea.converters.095',\
bnd.identity;id='org.eclipse.app4mc.amalthea.converters.096',\
bnd.identity;id='org.eclipse.app4mc.amalthea.converters.097',\
- osgi.identity;filter:='(&(osgi.identity=jaxen)(version>=1.1.6))'
+ osgi.identity;filter:='(&(osgi.identity=jaxen)(version>=1.1.6))',\
+ osgi.identity;filter:='(&(osgi.identity=org.apache.aries.jax.rs.jackson)(version>=1.0.2))'
-runfw: org.eclipse.osgi
-runee: JavaSE-1.8
@@ -30,8 +31,15 @@
-runbundles: \
ch.qos.logback.classic;version='[1.2.3,1.2.4)',\
ch.qos.logback.core;version='[1.2.3,1.2.4)',\
+ com.fasterxml.jackson.core.jackson-annotations;version='[2.11.2,2.11.3)',\
+ com.fasterxml.jackson.core.jackson-core;version='[2.11.2,2.11.3)',\
+ com.fasterxml.jackson.core.jackson-databind;version='[2.11.2,2.11.3)',\
+ com.fasterxml.jackson.jaxrs.jackson-jaxrs-base;version='[2.9.6,2.9.7)',\
+ com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider;version='[2.9.6,2.9.7)',\
+ com.fasterxml.jackson.module.jackson-module-jaxb-annotations;version='[2.9.6,2.9.7)',\
jaxen;version='[1.1.6,1.1.7)',\
org.apache.aries.javax.jax.rs-api;version='[1.0.0,1.0.1)',\
+ org.apache.aries.jax.rs.jackson;version='[1.0.2,1.0.3)',\
org.apache.aries.jax.rs.whiteboard;version='[1.0.1,1.0.2)',\
org.apache.felix.configadmin;version='[1.9.8,1.9.9)',\
org.apache.felix.configurator;version='[1.0.6,1.0.7)',\
@@ -40,23 +48,23 @@
org.apache.felix.scr;version='[2.1.10,2.1.11)',\
org.apache.servicemix.bundles.jdom;version='[2.0.6,2.0.7)',\
org.apache.servicemix.specs.annotation-api-1.3;version='[1.3.0,1.3.1)',\
- org.eclipse.app4mc.amalthea.converters.071;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.072;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.080;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.081;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.082;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.083;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.090;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.091;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.092;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.093;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.094;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.095;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.096;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.097;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.amalthea.converters.common;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.converter-app;version='[0.9.9,0.9.10)',\
- org.eclipse.app4mc.converter-service;version='[0.9.9,0.9.10)',\
+ org.eclipse.app4mc.amalthea.converters.071;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.072;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.080;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.081;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.082;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.083;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.090;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.091;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.092;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.093;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.094;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.095;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.096;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.097;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.amalthea.converters.common;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.converter-app;version='[1.0.0,1.0.1)',\
+ org.eclipse.app4mc.converter-service;version='[1.0.0,1.0.1)',\
org.eclipse.equinox.common;version='[3.12.0,3.12.1)',\
org.eclipse.osgi.services;version='[3.8.0,3.8.1)',\
org.eclipse.osgi.util;version='[3.5.300,3.5.301)',\
diff --git a/org.eclipse.app4mc.converter.cloud/converter-app/pom.xml b/org.eclipse.app4mc.converter.cloud/converter-app/pom.xml
index ec7faaa..e045039 100644
--- a/org.eclipse.app4mc.converter.cloud/converter-app/pom.xml
+++ b/org.eclipse.app4mc.converter.cloud/converter-app/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.eclipse.app4mc</groupId>
<artifactId>org.eclipse.app4mc.converter.cloud</artifactId>
- <version>0.9.9-SNAPSHOT</version>
+ <version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>org.eclipse.app4mc</groupId>
<artifactId>converter-service</artifactId>
- <version>0.9.9-SNAPSHOT</version>
+ <version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.osgi.enroute</groupId>
diff --git a/org.eclipse.app4mc.converter.cloud/converter-service/pom.xml b/org.eclipse.app4mc.converter.cloud/converter-service/pom.xml
index 34762ee..0add9b1 100644
--- a/org.eclipse.app4mc.converter.cloud/converter-service/pom.xml
+++ b/org.eclipse.app4mc.converter.cloud/converter-service/pom.xml
@@ -49,6 +49,26 @@
pom
</type>
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ <version>2.11.2</version>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <version>2.11.2</version>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>2.11.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.aries.jax.rs</groupId>
+ <artifactId>org.apache.aries.jax.rs.jackson</artifactId>
+ <version>1.0.2</version>
+ </dependency>
<!-- APP4MC Migration -->
<dependency>
<artifactId>
@@ -300,12 +320,12 @@
../pom.xml
</relativePath>
<version>
- 0.9.9-SNAPSHOT
+ 1.0.0-SNAPSHOT
</version>
</parent>
<properties>
<app4mc.version>
- 0.9.9-SNAPSHOT
+ 1.0.0-SNAPSHOT
</app4mc.version>
</properties>
</project>
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 d909002..6800b89 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
@@ -16,14 +16,20 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
@@ -31,14 +37,18 @@
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
import org.eclipse.app4mc.amalthea.converters.common.MigrationException;
import org.eclipse.app4mc.amalthea.converters.common.MigrationHelper;
@@ -49,12 +59,15 @@
import org.eclipse.app4mc.amalthea.converters.common.utils.ModelVersion;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(service=MigrationRestService.class)
@JaxrsResource
+@Produces(MediaType.APPLICATION_JSON)
+@JSONRequired
@Path("app4mc/converter")
public class MigrationRestService {
@@ -62,72 +75,169 @@
private static final Logger LOGGER = LoggerFactory.getLogger(MigrationRestService.class);
+ private static final String PROGRESS_MARKER = "in_progress";
+ private static final String ERROR_MARKER = "error";
+ private static final String FINISHED_MARKER = "finished";
+
+ private static final String ERROR_FILE = "error.txt";
+
private final String defaultBaseDir = System.getProperty("java.io.tmpdir");
+ private ExecutorService executor = Executors.newFixedThreadPool(1);
+
@Reference
MigrationProcessor migrationProcessor;
- @Path("upload")
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
- @Produces(MediaType.TEXT_HTML)
- public Response upload(@Context HttpServletRequest request) throws IOException, ServletException {
+ public Response upload(@Context HttpServletRequest request, @Context UriInfo uriInfo, @Context ServletContext context) throws IOException, ServletException {
+ Link self = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()).rel("self").build();
+
Part part = request.getPart("file");
- if (part != null) {
+ if (part != null && part.getSubmittedFileName() != null && part.getSubmittedFileName().length() > 0) {
String filename = part.getSubmittedFileName();
try (InputStream is = part.getInputStream()) {
- java.nio.file.Path path = Files.createTempDirectory(TEMP_DIR_PREFIX);
+ java.nio.file.Path tempFolderPath = Files.createTempDirectory(TEMP_DIR_PREFIX);
// extract uuid from pathname
- String uuid = path.toString().substring(path.toString().lastIndexOf('_') + 1);
+ String uuid = tempFolderPath.toString().substring(tempFolderPath.toString().lastIndexOf('_') + 1);
- Files.copy(is, Paths.get(path.toString(), filename));
+ // copy file to temporary location
+ java.nio.file.Path uploaded = Paths.get(tempFolderPath.toString(), filename);
+ Files.copy(is, uploaded);
- // return uuid
- return Response.ok(uuid).build();
+ if (Files.exists(uploaded)) {
+ // mark uuid in progress
+ getRegistry(context).put(uuid, PROGRESS_MARKER);
+
+ // trigger asynchronous processing
+ executor.execute(() -> {
+
+ String outputModelVersion = ModelVersion.getLatestVersion();
+
+ try (MigrationSettings migrationSettings = new MigrationSettings()) {
+ migrationSettings.setProject(uploaded.getParent().toFile());
+ migrationSettings.setMigrationModelVersion(outputModelVersion);
+
+ convert(Arrays.asList(uploaded.toFile()), migrationSettings, tempFolderPath, getRegistry(context));
+ } finally {
+ if (!ERROR_MARKER.equals(getRegistry(context).get(uuid))) {
+ getRegistry(context).put(uuid, FINISHED_MARKER);
+ }
+ }
+ });
+
+ // file upload succeeded and processing is triggered
+ Link statusLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
+ .path(uuid))
+ .rel("status")
+ .build();
+
+ return Response
+ .created(statusLink.getUri())
+ .entity(uuid)
+ .links(self, statusLink)
+ .build();
+ } else {
+ // file upload failed
+ return Response
+ .status(Status.NOT_FOUND)
+ .entity("Model file upload failed!")
+ .links(self)
+ .build();
+ }
}
}
- return Response.noContent().build();
+ return Response
+ .status(Status.BAD_REQUEST)
+ .entity("No model file provided!")
+ .links(self)
+ .build();
}
- @Path("{uuid}/convert")
- @PUT
- public Response convert(@PathParam("uuid") String uuid) throws IOException {
-
- java.nio.file.Path tempFolderPath = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
- List<java.nio.file.Path> modelFilePaths =
- Files.find(tempFolderPath, 1, (path, attrs) -> path.toString().endsWith(".amxmi"))
- .collect(Collectors.toList());
+ @Path("{uuid}")
+ @GET
+ public Response status(@PathParam("uuid") String uuid, @Context UriInfo uriInfo, @Context ServletContext context) throws IOException {
+ CacheControl cacheControl = new CacheControl();
+ cacheControl.setNoCache(true);
+ cacheControl.setNoStore(true);
- if (modelFilePaths.isEmpty()) {
+ Link self = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()).rel("self").build();
+
+ java.nio.file.Path tempFolderPath = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
+ if (!Files.exists(tempFolderPath)) {
return Response
- .status(Response.Status.NOT_FOUND)
- .entity("No model file uploaded!")
+ .status(Status.NOT_FOUND)
+ .entity("No status resource available for id " + uuid)
.build();
- }
-
- // build up MigrationSettings
- // same as AmaltheaModelMigrationHandler#collectInput
-
- String outputModelVersion = ModelVersion.getLatestVersion();
+ }
+
+ String status = getRegistry(context).get(uuid);
+
+ boolean hasErrorFile = false;
+ try (Stream<java.nio.file.Path> files = Files.list(tempFolderPath)) {
+ hasErrorFile = files.anyMatch(path -> path.endsWith(ERROR_FILE));
+ }
+
+ if (PROGRESS_MARKER.equals(status)) {
+ return Response
+ .accepted()
+ .links(self)
+ .cacheControl(cacheControl)
+ .build();
+ } else if (ERROR_MARKER.equals(status) || hasErrorFile) {
+ // processing finished with error
+ Link errorLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
+ .path("error"))
+ .rel("error")
+ .build();
- java.nio.file.Path modelFilePath = modelFilePaths.get(0);
- try (MigrationSettings migrationSettings = new MigrationSettings()) {
- migrationSettings.setProject(modelFilePath.getParent().toFile());
- migrationSettings.setMigrationModelVersion(outputModelVersion);
-
- // TODO execute in background thread to avoid timeout and provide status resource
- return convert(Arrays.asList(modelFilePath.toFile()), migrationSettings);
- }
+ return Response
+ .noContent()
+ .links(self, errorLink)
+ .cacheControl(cacheControl)
+ .build();
+ }
+
+ // processing is finished
+ Link downloadLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
+ .path("download"))
+ .rel("result")
+ .build();
+
+ return Response
+ .ok()
+ .links(self, downloadLink)
+ .cacheControl(cacheControl)
+ .build();
}
@Path("{uuid}/download")
@GET
- @Produces(MediaType.APPLICATION_OCTET_STREAM)
- public Response download(@PathParam("uuid") String uuid) throws IOException {
+ @Produces({ MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON } )
+ public Response download(@PathParam("uuid") String uuid, @Context UriInfo uriInfo, @Context ServletContext context) throws IOException {
+ Link self = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()).rel("self").build();
+
java.nio.file.Path tempFolderPath = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
+ if (!Files.exists(tempFolderPath)) {
+ return Response
+ .status(Status.NOT_FOUND)
+ .entity("No download resource available for id " + uuid)
+ .build();
+ }
+
+ // if process is in progress, the download resource is 404
+ String status = getRegistry(context).get(uuid);
+
+ if (PROGRESS_MARKER.equals(status)) {
+ return Response
+ .status(Status.NOT_FOUND)
+ .entity("Process is still in progresss")
+ .links(self)
+ .build();
+ }
List<java.nio.file.Path> modelFilePaths =
Files.find(tempFolderPath, 1, (path, attrs) -> path.toString().endsWith(".amxmi"))
@@ -135,28 +245,90 @@
if (modelFilePaths.isEmpty()) {
return Response
- .status(Response.Status.NOT_FOUND)
+ .status(Status.NOT_FOUND)
.entity("No migrated model file available!")
+ .links(self)
.build();
}
java.nio.file.Path modelFilePath = modelFilePaths.get(0);
- ResponseBuilder response = Response.ok(modelFilePath.toFile());
- response.header("Content-Disposition", "attachment; filename=\"" + modelFilePath.toFile().getName() + "\"");
- return response.build();
+ List<PathSegment> pathSegments = uriInfo.getPathSegments();
+ UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder().replacePath("");
+ for (int i = 0; i < pathSegments.size() - 1; i++) {
+ uriBuilder.path(pathSegments.get(i).getPath());
+ }
+ Link deleteLink = Link.fromUriBuilder(uriBuilder)
+ .rel("delete")
+ .build();
+
+ return Response
+ .ok(modelFilePath.toFile())
+ .header("Content-Disposition", "attachment; filename=\"" + modelFilePath.toFile().getName() + "\"")
+ .links(self, deleteLink)
+ .build();
}
- @Path("{uuid}/delete")
+ @Path("{uuid}/error")
+ @GET
+ @Produces({ MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON } )
+ public Response error(@PathParam("uuid") String uuid, @Context UriInfo uriInfo) throws IOException {
+ Link self = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()).rel("self").build();
+
+ java.nio.file.Path tempFolderPath = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
+
+ if (!Files.exists(tempFolderPath)) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ boolean hasErrorFile = false;
+ try (Stream<java.nio.file.Path> files = Files.list(tempFolderPath)) {
+ hasErrorFile = files.anyMatch(path -> path.endsWith(ERROR_FILE));
+ }
+
+ // if there is no error file, the error resource is 404
+ if (!hasErrorFile) {
+ return Response
+ .status(Status.NOT_FOUND)
+ .entity("No error occured")
+ .links(self)
+ .build();
+ }
+
+ java.nio.file.Path errorFilePath = Paths.get(tempFolderPath.toString(), ERROR_FILE);
+
+ List<PathSegment> pathSegments = uriInfo.getPathSegments();
+ UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder().replacePath("");
+ for (int i = 0; i < pathSegments.size() - 1; i++) {
+ uriBuilder.path(pathSegments.get(i).getPath());
+ }
+ Link deleteLink = Link.fromUriBuilder(uriBuilder)
+ .rel("delete")
+ .build();
+
+ return Response
+ .ok(errorFilePath.toFile())
+ .header("Content-Disposition", "attachment; filename=\"" + errorFilePath.toFile().getName() + "\"")
+ .links(self, deleteLink)
+ .build();
+ }
+
+ @Path("{uuid}")
@DELETE
- public Response delete(@PathParam("uuid") String uuid) throws IOException {
+ public Response delete(@PathParam("uuid") String uuid, @Context ServletContext context) throws IOException {
java.nio.file.Path path = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
+ if (!Files.exists(path)) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
Files.walk(path)
.sorted(Comparator.reverseOrder())
.map(java.nio.file.Path::toFile)
.forEach(File::delete);
+ getRegistry(context).remove(uuid);
+
return Response.ok().build();
}
@@ -165,7 +337,8 @@
* @param inputFiles
* @param migrationSettings
*/
- private Response convert(List<File> inputFiles, MigrationSettings migrationSettings) {
+ private void convert(List<File> inputFiles, MigrationSettings migrationSettings, java.nio.file.Path tempFolderPath, Map<String, String> statusMap) {
+ String uuid = tempFolderPath.toString().substring(tempFolderPath.toString().lastIndexOf('_') + 1);
// same as ModelLoaderJob
try {
@@ -173,10 +346,9 @@
migrationSettings.getMigModelFiles().addAll(modelFiles);
} catch (Exception e) {
LOGGER.error("Failed to load model files", e);
- return Response
- .status(Response.Status.BAD_REQUEST)
- .entity("Failed to load model files")
- .build();
+ statusMap.put(uuid, ERROR_MARKER);
+ error(tempFolderPath, "Failed to load model files", e);
+ return;
}
// same as AmaltheaModelMigrationHandler#JobChangeListener
@@ -184,10 +356,9 @@
boolean inputValid = MigrationHelper.isInputModelVersionValid(migrationSettings);
if (!inputValid) {
LOGGER.error("Model migration stopped in {} as selected model files belong to different versions", migrationSettings.getOutputDirectoryLocation());
- return Response
- .status(Response.Status.BAD_REQUEST)
- .entity("Model migration stopped as selected model files belong to different versions")
- .build();
+ statusMap.put(uuid, ERROR_MARKER);
+ error(tempFolderPath, "Model migration stopped as selected model files belong to different versions", null);
+ return;
} else {
if (migrationSettings.getInputModelVersion() != null
@@ -196,10 +367,7 @@
LOGGER.error("Selected model is compatible to latest AMALTHEA meta-model version {}.\nIt is not required to migrate the model in {}",
ModelVersion.getLatestVersion(),
migrationSettings.getOutputDirectoryLocation());
- return Response
- .status(Response.Status.BAD_REQUEST)
- .entity("Selected model is compatible to latest AMALTHEA meta-model version " + ModelVersion.getLatestVersion() + ".\\nIt is not required to migrate the model.")
- .build();
+ return;
} else {
// check if a migration needs to be executed
Map<String, String> migStepEntries = MigrationHelper.generateMigrationSteps(
@@ -210,13 +378,14 @@
LOGGER.error("Migration not supported for the selected model versions.\nInput Model version : \"{}\" Output Model Version : \"{}\"",
migrationSettings.getInputModelVersion(),
migrationSettings.getMigrationModelVersion());
- return Response
- .status(Response.Status.BAD_REQUEST)
- .entity("Migration not supported for the selected model versions.\nInput Model version : "
- + migrationSettings.getInputModelVersion()
+ statusMap.put(uuid, ERROR_MARKER);
+ error(tempFolderPath,
+ "Migration not supported for the selected model versions.\nInput Model version : "
+ + migrationSettings.getInputModelVersion()
+ " Output Model Version : "
- + migrationSettings.getMigrationModelVersion())
- .build();
+ + migrationSettings.getMigrationModelVersion(),
+ null);
+ return;
}
// set the file parent folder as output location to convert the file at source
@@ -235,19 +404,19 @@
+ migrationSettings.getInputModelVersion()
+ "\" Output Model Version : \""
+ migrationSettings.getMigrationModelVersion() + "\"");
- return Response
- .status(Response.Status.BAD_REQUEST)
- .entity("Migration not supported for the selected model versions.\nInput Model version : "
- + migrationSettings.getInputModelVersion()
+ statusMap.put(uuid, ERROR_MARKER);
+ error(tempFolderPath,
+ "Migration not supported for the selected model versions.\nInput Model version : "
+ + migrationSettings.getInputModelVersion()
+ " Output Model Version : "
- + migrationSettings.getMigrationModelVersion())
- .build();
+ + migrationSettings.getMigrationModelVersion(),
+ null);
+ return;
case MigrationStatusCode.ERROR:
LOGGER.error("Error during migration in {}", migrationSettings.getOutputDirectoryLocation());
- return Response
- .status(Response.Status.BAD_REQUEST)
- .entity("Error during migration.")
- .build();
+ statusMap.put(uuid, ERROR_MARKER);
+ error(tempFolderPath, "Error during migration.", null);
+ return;
default:
LOGGER.info("Model Migration in {} successful !!", migrationSettings.getOutputDirectoryLocation());
}
@@ -255,13 +424,31 @@
}
} catch (MigrationException e) {
LOGGER.error("Error during migration in {} : {}", migrationSettings.getOutputDirectoryLocation(), e.getLocalizedMessage());
- return Response
- .status(Response.Status.BAD_REQUEST)
- .entity("Error during migration. " + e.getLocalizedMessage())
- .build();
+ statusMap.put(uuid, ERROR_MARKER);
+ error(tempFolderPath, "Error during migration.", e);
}
-
- return Response.ok("Model Migration successful!").build();
}
+ private void error(java.nio.file.Path resultFolder, String message, Exception exception) {
+ try {
+ java.nio.file.Path errorFilePath = Files.createFile(Paths.get(resultFolder.toString(), ERROR_FILE));
+ try (PrintWriter writer = new PrintWriter(Files.newOutputStream(errorFilePath))) {
+ writer.append(message).append(System.lineSeparator());
+ if (exception != null) {
+ exception.printStackTrace(writer);
+ }
+ }
+
+ } catch (IOException e) {
+ LOGGER.error("Failed to write error.txt", e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static synchronized Map<String, String> getRegistry(ServletContext context) {
+ if (context.getAttribute("_REGISTRY") == null) {
+ context.setAttribute("_REGISTRY", new ConcurrentHashMap<String, String>());
+ }
+ return (Map<String, String>) context.getAttribute("_REGISTRY");
+ }
}
diff --git a/org.eclipse.app4mc.converter.cloud/openapi.yaml b/org.eclipse.app4mc.converter.cloud/openapi.yaml
new file mode 100644
index 0000000..78d0491
--- /dev/null
+++ b/org.eclipse.app4mc.converter.cloud/openapi.yaml
@@ -0,0 +1,174 @@
+openapi: 3.0.0
+info:
+ version: 0.9.9
+ 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
+
+paths:
+ /:
+ post:
+ summary: Upload the file to migrate and start the migration process asynchronously
+ requestBody:
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ file:
+ type: string
+ format: binary
+ responses:
+ '201':
+ description: Upload succeeded and migration process started
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ description: ID of the created migration 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 migration 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: Migration 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 transformed 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 migrated model file
+ operationId: getDownload
+ parameters:
+ - in: path
+ name: statusId
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Migration result available
+ 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 migration 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
diff --git a/org.eclipse.app4mc.converter.cloud/pom.xml b/org.eclipse.app4mc.converter.cloud/pom.xml
index 6082ec2..d8de7da 100644
--- a/org.eclipse.app4mc.converter.cloud/pom.xml
+++ b/org.eclipse.app4mc.converter.cloud/pom.xml
@@ -7,7 +7,7 @@
org.eclipse.app4mc.converter.cloud
</artifactId>
<version>
- 0.9.9-SNAPSHOT
+ 1.0.0-SNAPSHOT
</version>
<packaging>
pom
diff --git a/org.eclipse.app4mc.validation.cloud/Dockerfile b/org.eclipse.app4mc.validation.cloud/Dockerfile
new file mode 100644
index 0000000..6b9b08f
--- /dev/null
+++ b/org.eclipse.app4mc.validation.cloud/Dockerfile
@@ -0,0 +1,6 @@
+FROM amazoncorretto:8u265
+
+ADD org.eclipse.app4mc.validation.cloud.product/target/products/org.eclipse.app4mc.validation.cloud.product-linux.gtk.x86_64.tar.gz org.eclipse.app4mc.validation.cloud.product
+RUN chmod +x org.eclipse.app4mc.validation.cloud.product/eclipse
+
+ENTRYPOINT org.eclipse.app4mc.validation.cloud.product/eclipse
\ No newline at end of file
diff --git a/org.eclipse.app4mc.validation.cloud/openapi.yaml b/org.eclipse.app4mc.validation.cloud/openapi.yaml
new file mode 100644
index 0000000..61f8369
--- /dev/null
+++ b/org.eclipse.app4mc.validation.cloud/openapi.yaml
@@ -0,0 +1,200 @@
+openapi: 3.0.0
+info:
+ version: 0.9.9
+ title: APP4MC Validation API
+ description: APP4MC Validation API to validate an Amalthea model file
+
+servers:
+ - url: http://localhost:8080/app4mc/validation
+
+paths:
+ /:
+ post:
+ summary: Upload the file to validate and start the validation process asynchronously
+ requestBody:
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ file:
+ type: string
+ format: binary
+ responses:
+ '201':
+ description: Upload succeeded and validation process started
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ description: ID of the created validation 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 validation 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: Validation 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 validation result file
+ operationId: getDownload
+ parameters:
+ - in: path
+ name: statusId
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Validation successful
+ content:
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ headers:
+ # custom header to configure that the result should not be used as input to the next process in the workflow
+ x-app4mc-use-result:
+ schema:
+ type: string
+ enum:
+ - false
+ links:
+ delete:
+ operationId: deleteResource
+ parameters:
+ statusId: $request.path.statusId
+ '400':
+ description: Validation finished with validation failures
+ content:
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ headers:
+ # custom header to configure that the result should not be used as input to the next process in the workflow
+ x-app4mc-use-result:
+ schema:
+ type: string
+ enum:
+ - false
+ 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 migration 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
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 6f79912..1096c34 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
@@ -9,7 +9,8 @@
com.fasterxml.jackson.databind;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";resolution:=optional
+ org.osgi.service.component.annotations;version="1.3.0";resolution:=optional,
+ org.slf4j;version="1.7.2"
Require-Bundle: org.eclipse.app4mc.amalthea.model;bundle-version="0.9.9",
org.eclipse.app4mc.validation.core;bundle-version="0.9.9"
Bundle-ActivationPolicy: lazy
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 4a47952..82c54e8 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
@@ -19,15 +19,22 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
+import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@@ -42,6 +49,8 @@
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ServiceScope;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -54,13 +63,26 @@
},
scope=ServiceScope.PROTOTYPE)
public class ValidationServlet extends HttpServlet {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ValidationServlet.class);
private static final long serialVersionUID = 1L;
private static final String TEMP_DIR_PREFIX = "app4mc_validation_";
+ private static final String PROGRESS_MARKER = "in_progress";
+ private static final String ERROR_MARKER = "error";
+ private static final String FINISHED_MARKER = "finished";
+
+ private static final String ERROR_FILE = "error.txt";
+
+ private static final String PASSED_MARKER = "passed";
+ private static final String FAILED_MARKER = "failed";
+
private final String defaultBaseDir = System.getProperty("java.io.tmpdir");
-
+
+ private ExecutorService executor = Executors.newFixedThreadPool(1);
+
@Reference
private ProfileManager manager;
@@ -70,6 +92,10 @@
return null;
}
+ if (pathInfo.startsWith("/")) {
+ pathInfo.substring(1);
+ }
+
String[] splitPath = pathInfo.split("/");
if (splitPath.length > 3) {
@@ -79,7 +105,7 @@
return splitPath;
}
- // POST /app4mc/validation/upload
+ // POST /app4mc/validation
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
@@ -87,116 +113,118 @@
String[] splitPath = validatePath(request.getPathInfo());
- if (splitPath == null || splitPath.length != 2) {
+ String requestUrl = request.getRequestURL().toString();
+ if (requestUrl.endsWith("/")) {
+ requestUrl = requestUrl.substring(0, requestUrl.length() - 1);
+ }
+
+ if (splitPath != null && splitPath.length != 1) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
+ response.addHeader("Link", "<" + requestUrl + ">;rel=\"self\"");
return;
}
- if ("upload".equals(splitPath[1])){
- Part part = request.getPart("file");
- if (part != null) {
- String filename = part.getSubmittedFileName();
- try (InputStream is = part.getInputStream()) {
- Path path = Files.createTempDirectory(TEMP_DIR_PREFIX);
-
- // extract uuid from pathname
- String uuid = path.toString().substring(path.toString().lastIndexOf('_') + 1);
-
- Files.copy(is, Paths.get(path.toString(), filename));
-
- // return uuid
- response.setContentType("text/html");
- response.getWriter().write(uuid);
- return;
- }
- }
- }
-
- // no content
- response.setStatus(HttpServletResponse.SC_NO_CONTENT);
- return;
- }
-
- // PUT /app4mc/validation/{id}/validate
-
- @Override
- protected void doPut(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
-
- String[] splitPath = validatePath(request.getPathInfo());
-
- if (splitPath == null || splitPath.length != 3) {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
- return;
- }
+ Part part = request.getPart("file");
+ if (part != null && part.getSubmittedFileName() != null && part.getSubmittedFileName().length() > 0) {
+ String filename = part.getSubmittedFileName();
+ try (InputStream is = part.getInputStream()) {
+ Path tempFolderPath = Files.createTempDirectory(TEMP_DIR_PREFIX);
+
+ // extract uuid from pathname
+ String uuid = tempFolderPath.toString().substring(tempFolderPath.toString().lastIndexOf('_') + 1);
+
+ Path uploaded = Paths.get(tempFolderPath.toString(), filename);
+ Files.copy(is, uploaded);
+
+ if (Files.exists(uploaded)) {
+ // mark uuid in progress
+ ServletContext context = getServletContext();
+ Map<String, String> registry = getRegistry(context);
+ registry.put(uuid, PROGRESS_MARKER);
+
+ // trigger asynchronous processing
+ executor.execute(() -> {
- String uuid = splitPath[1];
- String action = splitPath[2];
-
- if ("validate".equals(action)) {
- // trigger validation
- Path tempFolderPath = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
- List<Path> modelFilePaths =
- Files.find(tempFolderPath, 1, (path, attrs) -> path.toString().endsWith(".amxmi"))
- .collect(Collectors.toList());
-
- if (modelFilePaths.isEmpty()) {
- response.sendError(HttpServletResponse.SC_NOT_FOUND, "No model file uploaded!");
- return;
- }
-
- Path modelFilePath = modelFilePaths.get(0);
-
- // load uploaded model file
- Amalthea model = AmaltheaLoader.loadFromFile(modelFilePath.toFile());
-
- if (model == null) {
- response.sendError(HttpServletResponse.SC_NOT_FOUND, "Error: No model loaded!");
- return;
- }
-
- // get profile selection out of request
- String[] profiles = request.getParameterValues("profiles");
- List<String> selectedProfiles = profiles != null ? Arrays.asList(profiles) : Arrays.asList("Amalthea Standard Validations");
-
- // get selected profiles from profile manager
- List<Class<? extends IProfile>> profileList = manager.getRegisteredValidationProfiles().values().stream()
- .filter(profile -> selectedProfiles.contains(profile.getName()))
- .map(profile -> profile.getProfileClass())
- .collect(Collectors.toList());
-
- // TODO execute in background thread and introduce status resource?
- ValidationExecutor executor = new ValidationExecutor(profileList);
- executor.validate(model);
-
- // dump validation result to file
- Path resultFile = Files.createFile(Paths.get(tempFolderPath.toString(), "validation-results.txt"));
- try (PrintStream print = new PrintStream(new FileOutputStream(resultFile.toFile()))) {
- executor.dumpResultMap(print);
- }
+ try {
+ // load uploaded model file
+ Amalthea model = AmaltheaLoader.loadFromFile(uploaded.toFile());
+
+ if (model == null) {
+ registry.put(uuid, ERROR_MARKER);
+ error(tempFolderPath, "Error: No model loaded!", null);
+ return;
+ }
+
+ // get profile selection out of request
+ String[] profiles = request.getParameterValues("profiles");
+ List<String> selectedProfiles = profiles != null ? Arrays.asList(profiles) : Arrays.asList("Amalthea Standard Validations");
+
+ // get selected profiles from profile manager
+ List<Class<? extends IProfile>> profileList = manager.getRegisteredValidationProfiles().values().stream()
+ .filter(profile -> selectedProfiles.contains(profile.getName()))
+ .map(profile -> profile.getProfileClass())
+ .collect(Collectors.toList());
+
+ ValidationExecutor executor = new ValidationExecutor(profileList);
+ executor.validate(model);
+
+ // dump validation result to file
+ Path resultFile = Files.createFile(Paths.get(tempFolderPath.toString(), "validation-results.txt"));
+ try (PrintStream print = new PrintStream(new FileOutputStream(resultFile.toFile()))) {
+ executor.dumpResultMap(print);
+ }
- boolean failed = executor.getResults().stream()
- .anyMatch(diag -> diag.getSeverity() == org.eclipse.emf.common.util.Diagnostic.ERROR);
- if (!failed) {
- response.setStatus(HttpServletResponse.SC_OK);
- } else {
- response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
+ boolean failed = executor.getResults().stream()
+ .anyMatch(diag -> diag.getSeverity() == org.eclipse.emf.common.util.Diagnostic.ERROR);
+ if (!failed) {
+ Files.createFile(Paths.get(tempFolderPath.toString(), PASSED_MARKER));
+ } else {
+ Files.createFile(Paths.get(tempFolderPath.toString(), FAILED_MARKER));
+ }
+ } catch (IOException e) {
+ LOGGER.error("Failed to write validation results", e);
+ registry.put(uuid, ERROR_MARKER);
+ error(tempFolderPath, "Failed to write validation results", e);
+ } finally {
+ if (!ERROR_MARKER.equals(getRegistry(context).get(uuid))) {
+ getRegistry(context).put(uuid, FINISHED_MARKER);
+ }
+ }
+ });
+
+ // return uuid
+ response.setStatus(HttpServletResponse.SC_CREATED);
+ response.addHeader("Location", requestUrl + "/" + uuid);
+ response.addHeader("Link", "<" + requestUrl + ">;rel=\"self\"");
+ response.addHeader("Link", "<" + requestUrl + "/" + uuid + ">;rel=\"status\"");
+ response.setContentType("application/json");
+ response.getWriter().write(uuid);
+ return;
+ } else {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, "Model file upload failed!");
+ response.addHeader("Link", "<" + requestUrl + ">;rel=\"self\"");
+ return;
+ }
}
- return;
}
- // no content
- response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ // bad request without file
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, "No model file provided!");
+ response.addHeader("Link", "<" + request.getRequestURL() + ">;rel=\"self\"");
return;
}
// GET /app4mc/validation/profiles
+ // GET /app4mc/validation/{id}
// GET /app4mc/validation/{id}/download
+ // GET /app4mc/validation/{id}/error
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
+ response.addHeader("Link", "<" + request.getRequestURL() + ">;rel=\"self\"");
+
String[] splitPath = validatePath(request.getPathInfo());
if (splitPath == null) {
@@ -219,11 +247,77 @@
}
return;
- } else if (splitPath.length == 3 && "download".equals(splitPath[2])) {
- Path path = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + splitPath[1], "validation-results.txt");
+ } else if (splitPath.length == 2) {
+ response.setHeader("Cache-Control", "private, no-store, no-cache, must-revalidate");
+
+ String uuid = splitPath[1];
+ // check for the in_progress marker file
+ Path tempFolderPath = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
+ if (!Files.exists(tempFolderPath)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ boolean hasErrorFile = false;
+ try (Stream<Path> files = Files.list(tempFolderPath)) {
+ hasErrorFile = files.anyMatch(path -> path.endsWith(ERROR_FILE));
+ }
+
+ String status = getRegistry(getServletContext()).get(uuid);
+ if (PROGRESS_MARKER.equals(status)) {
+ response.setStatus(HttpServletResponse.SC_ACCEPTED);
+ return;
+ } else if (ERROR_MARKER.equals(status) || hasErrorFile) {
+ // processing finished with error
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ response.addHeader("Link", "<" + request.getRequestURL() + "/error>;rel=\"error\"");
+ return;
+ }
+
+ // processing is finished
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.addHeader("Link", "<" + request.getRequestURL() + "/download>;rel=\"result\"");
+ return;
+
+ } else if (splitPath.length == 3 && "download".equals(splitPath[2])) {
+ String status = getRegistry(getServletContext()).get(splitPath[1]);
+
+ if (PROGRESS_MARKER.equals(status)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, "Process is still in progresss");
+ return;
+ }
+
+ Path tempFolderPath = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + splitPath[1]);
+
+ if (!Files.exists(tempFolderPath)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ // set status based on validation result
+ boolean hasFailedMarker = false;
+ try (Stream<Path> files = Files.list(tempFolderPath)) {
+ hasFailedMarker = files.anyMatch(p -> p.endsWith(FAILED_MARKER));
+ }
+
+ if (hasFailedMarker) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ } else {
+ response.setStatus(HttpServletResponse.SC_OK);
+ }
+
+ Path path = Paths.get(tempFolderPath.toString(), "validation-results.txt");
+ if (!Files.exists(path)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, "No validation result available");
+ return;
+ }
+
+ response.addHeader("Link", "<" + request.getRequestURL().substring(0, request.getRequestURL().lastIndexOf("/")) + ">;rel=\"delete\"");
+ response.setHeader("Content-Disposition","attachment; filename=\"" + path.toFile().getName() + "\"");
+ // set special header to tell that the result should not be used in the next process step
+ response.setHeader("x-app4mc-use-result", "false");
response.setContentType("text/plain");
- response.setHeader("Content-Disposition","attachment; filename=validation-results.txt");
try (InputStream in = Files.newInputStream(path);
OutputStream out = response.getOutputStream()) {
@@ -235,6 +329,45 @@
out.write(buffer, 0, numBytesRead);
}
}
+ return;
+ } else if (splitPath.length == 3 && "error".equals(splitPath[2])) {
+ String uuid = splitPath[1];
+ Path tempFolderPath = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
+
+ if (!Files.exists(tempFolderPath)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ // if there is no error file, the error resource is 404
+ boolean hasErrorFile = false;
+ try (Stream<Path> files = Files.list(tempFolderPath)) {
+ hasErrorFile = files.anyMatch(path -> path.endsWith(ERROR_FILE));
+ }
+
+ if (!hasErrorFile) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, "No error occured");
+ return;
+ }
+
+ Path errorFilePath = Paths.get(tempFolderPath.toString(), ERROR_FILE);
+
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.addHeader("Link", "<" + request.getRequestURL().substring(0, request.getRequestURL().lastIndexOf("/")) + ">;rel=\"delete\"");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + errorFilePath.toFile().getName() + "\"");
+ response.setContentType("text/plain");
+
+ try (InputStream in = Files.newInputStream(errorFilePath);
+ OutputStream out = response.getOutputStream()) {
+
+ byte[] buffer = new byte[4096];
+
+ int numBytesRead;
+ while ((numBytesRead = in.read(buffer)) > 0) {
+ out.write(buffer, 0, numBytesRead);
+ }
+ }
+ return;
}
// no content
@@ -242,36 +375,59 @@
return;
}
- // DELETE /app4mc/validation/{id}/delete
+
+ // DELETE /app4mc/validation/{id}
@Override
protected void doDelete(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String[] splitPath = validatePath(request.getPathInfo());
-
- if (splitPath == null || splitPath.length != 3) {
+
+ if (splitPath == null || splitPath.length != 2) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
return;
}
String uuid = splitPath[1];
- String action = splitPath[2];
- if ("delete".equals(action)) {
- Path path = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
-
- Files.walk(path)
- .sorted(Comparator.reverseOrder())
- .map(Path::toFile)
- .forEach(File::delete);
-
- response.setStatus(HttpServletResponse.SC_OK);
- return;
- }
+ Path path = Paths.get(defaultBaseDir, TEMP_DIR_PREFIX + uuid);
+
+ if (Files.exists(path)) {
+ Files.walk(path)
+ .sorted(Comparator.reverseOrder())
+ .map(Path::toFile)
+ .forEach(File::delete);
+
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ // not found
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
- // no content
- response.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
+
+ private void error(Path resultFolder, String message, Exception exception) {
+ try {
+ Path errorFilePath = Files.createFile(Paths.get(resultFolder.toString(), ERROR_FILE));
+ try (PrintWriter writer = new PrintWriter(Files.newOutputStream(errorFilePath))) {
+ writer.append(message).append(System.lineSeparator());
+ if (exception != null) {
+ exception.printStackTrace(writer);
+ }
+ }
+
+ } catch (IOException e) {
+ LOGGER.error("Failed to write error.txt", e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static synchronized Map<String, String> getRegistry(ServletContext context) {
+ if (context.getAttribute("_REGISTRY") == null) {
+ context.setAttribute("_REGISTRY", new ConcurrentHashMap<String, String>());
+ }
+ return (Map<String, String>) context.getAttribute("_REGISTRY");
+ }
}
\ No newline at end of file
diff --git a/org.eclipse.app4mc.validation.cloud/pom.xml b/org.eclipse.app4mc.validation.cloud/pom.xml
index 53f3521..f62fc1a 100644
--- a/org.eclipse.app4mc.validation.cloud/pom.xml
+++ b/org.eclipse.app4mc.validation.cloud/pom.xml
@@ -97,6 +97,27 @@
<groupId>org.eclipse.tycho</groupId>
<artifactId>tycho-p2-director-plugin</artifactId>
<version>${tycho.version}</version>
+ <executions>
+ <execution>
+ <id>materialize-products</id>
+ <goals>
+ <goal>materialize-products</goal>
+ </goals>
+ </execution>
+
+ <execution>
+ <id>archive-products</id>
+ <goals>
+ <goal>archive-products</goal>
+ </goals>
+ <configuration>
+ <formats>
+ <linux>tar.gz</linux>
+ <macosx>tar.gz</macosx>
+ </formats>
+ </configuration>
+ </execution>
+ </executions>
</plugin>
</plugins>
</pluginManagement>
diff --git a/start.bat b/start.bat
new file mode 100644
index 0000000..e3e2607
--- /dev/null
+++ b/start.bat
@@ -0,0 +1,5 @@
+start java -jar org.eclipse.app4mc.converter.cloud/converter-app/target/converter-app.jar
+start org.eclipse.app4mc.validation.cloud/org.eclipse.app4mc.validation.cloud.product/target/products/org.eclipse.app4mc.validation.cloud.product/win32/win32/x86_64/eclipse.exe -console
+start java -jar manager/target/manager-0.0.1-SNAPSHOT.jar
+timeout /T 10
+start firefox.exe "http://localhost:9090"
\ No newline at end of file