blob: 6800b89b2af77b8f6f107280e2a8e8cd4e34afd7 [file] [log] [blame]
/*********************************************************************************
* 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.org.eclipse.app4mc.converter.cloud;
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;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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.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;
import org.eclipse.app4mc.amalthea.converters.common.MigrationInputFile;
import org.eclipse.app4mc.amalthea.converters.common.MigrationProcessor;
import org.eclipse.app4mc.amalthea.converters.common.MigrationSettings;
import org.eclipse.app4mc.amalthea.converters.common.MigrationStatusCode;
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 {
private static final String TEMP_DIR_PREFIX = "app4mc_migration_";
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;
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
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 && part.getSubmittedFileName() != null && part.getSubmittedFileName().length() > 0) {
String filename = part.getSubmittedFileName();
try (InputStream is = part.getInputStream()) {
java.nio.file.Path tempFolderPath = Files.createTempDirectory(TEMP_DIR_PREFIX);
// extract uuid from pathname
String uuid = tempFolderPath.toString().substring(tempFolderPath.toString().lastIndexOf('_') + 1);
// copy file to temporary location
java.nio.file.Path uploaded = Paths.get(tempFolderPath.toString(), filename);
Files.copy(is, uploaded);
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
.status(Status.BAD_REQUEST)
.entity("No model file provided!")
.links(self)
.build();
}
@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);
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 status resource available for id " + uuid)
.build();
}
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();
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, 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"))
.collect(Collectors.toList());
if (modelFilePaths.isEmpty()) {
return Response
.status(Status.NOT_FOUND)
.entity("No migrated model file available!")
.links(self)
.build();
}
java.nio.file.Path modelFilePath = modelFilePaths.get(0);
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}/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, @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();
}
/**
* Convert the given list of files.
* @param inputFiles
* @param 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 {
List<MigrationInputFile> modelFiles = MigrationHelper.populateModels(inputFiles, migrationSettings);
migrationSettings.getMigModelFiles().addAll(modelFiles);
} catch (Exception e) {
LOGGER.error("Failed to load model files", e);
statusMap.put(uuid, ERROR_MARKER);
error(tempFolderPath, "Failed to load model files", e);
return;
}
// same as AmaltheaModelMigrationHandler#JobChangeListener
try {
boolean inputValid = MigrationHelper.isInputModelVersionValid(migrationSettings);
if (!inputValid) {
LOGGER.error("Model migration stopped in {} as selected model files belong to different versions", migrationSettings.getOutputDirectoryLocation());
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
&& ModelVersion.getLatestVersion().equals(migrationSettings.getInputModelVersion())) {
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;
} else {
// check if a migration needs to be executed
Map<String, String> migStepEntries = MigrationHelper.generateMigrationSteps(
migrationSettings.getInputModelVersion(),
migrationSettings.getMigrationModelVersion());
if (migStepEntries.size() == 0) {
LOGGER.error("Migration not supported for the selected model versions.\nInput Model version : \"{}\" Output Model Version : \"{}\"",
migrationSettings.getInputModelVersion(),
migrationSettings.getMigrationModelVersion());
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(),
null);
return;
}
// set the file parent folder as output location to convert the file at source
MigrationInputFile migrationInputFile = migrationSettings.getMigModelFiles().get(0);
migrationSettings.setOutputDirectoryLocation(migrationInputFile.getOriginalFile().getParent());
//now call migration job to migrate the file to latest Amalthea version
// same as ModelMigrationJob
int result = migrationProcessor.execute(migrationSettings, null);
switch (result) {
case MigrationStatusCode.UNSUPPORTED_MODEL_VERSIONS:
LOGGER.error("Migration in "
+ migrationSettings.getOutputDirectoryLocation()
+ " not supported for the selected model versions. \nInput Model version : \""
+ migrationSettings.getInputModelVersion()
+ "\" Output Model Version : \""
+ migrationSettings.getMigrationModelVersion() + "\"");
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(),
null);
return;
case MigrationStatusCode.ERROR:
LOGGER.error("Error during migration in {}", migrationSettings.getOutputDirectoryLocation());
statusMap.put(uuid, ERROR_MARKER);
error(tempFolderPath, "Error during migration.", null);
return;
default:
LOGGER.info("Model Migration in {} successful !!", migrationSettings.getOutputDirectoryLocation());
}
}
}
} catch (MigrationException e) {
LOGGER.error("Error during migration in {} : {}", migrationSettings.getOutputDirectoryLocation(), e.getLocalizedMessage());
statusMap.put(uuid, ERROR_MARKER);
error(tempFolderPath, "Error during migration.", e);
}
}
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");
}
}