| /********************************************************************************* |
| * 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"); |
| } |
| } |