| /********************************************************************************* |
| * 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.validation.cloud.http; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| 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; |
| import javax.servlet.http.HttpServletResponse; |
| import javax.servlet.http.Part; |
| |
| import org.eclipse.app4mc.amalthea.model.Amalthea; |
| import org.eclipse.app4mc.amalthea.model.io.AmaltheaLoader; |
| import org.eclipse.app4mc.validation.core.IProfile; |
| import org.eclipse.app4mc.validation.util.ProfileManager; |
| import org.eclipse.app4mc.validation.util.ValidationExecutor; |
| 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; |
| |
| @Component( |
| service=Servlet.class, |
| property= { |
| "osgi.http.whiteboard.servlet.pattern=/app4mc/validation/*", |
| "osgi.http.whiteboard.servlet.multipart.enabled=true" |
| }, |
| 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; |
| |
| private String[] validatePath(String pathInfo) { |
| // request to /app4mc/validation |
| if (pathInfo == null || pathInfo.equals("/")){ |
| return null; |
| } |
| |
| if (pathInfo.startsWith("/")) { |
| pathInfo.substring(1); |
| } |
| |
| String[] splitPath = pathInfo.split("/"); |
| |
| if (splitPath.length > 3) { |
| return null; |
| } |
| |
| return splitPath; |
| } |
| |
| // POST /app4mc/validation |
| |
| @Override |
| protected void doPost(HttpServletRequest request, HttpServletResponse response) |
| throws ServletException, IOException { |
| |
| String[] splitPath = validatePath(request.getPathInfo()); |
| |
| 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; |
| } |
| |
| 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(() -> { |
| |
| 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) { |
| 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; |
| } |
| } |
| } |
| |
| // 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) { |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path"); |
| return; |
| } |
| |
| if (splitPath.length == 2 && "profiles".equals(splitPath[1])) { |
| response.setContentType("application/json"); |
| ObjectMapper objectMapper = new ObjectMapper(); |
| try { |
| List<String> availableProfiles = manager.getRegisteredValidationProfiles().values().stream() |
| .sorted((p1, p2) -> p1.getName().compareTo(p2.getName())) |
| .map(profile -> profile.getName()) |
| .collect(Collectors.toList()); |
| String json = objectMapper.writeValueAsString(availableProfiles); |
| response.getWriter().write(json); |
| } catch (JsonProcessingException e) { |
| response.getWriter().write(objectMapper.writeValueAsString(e)); |
| } |
| |
| return; |
| } 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"); |
| |
| try (InputStream in = Files.newInputStream(path); |
| OutputStream out = response.getOutputStream()) { |
| |
| byte[] buffer = new byte[4096]; |
| |
| int numBytesRead; |
| while ((numBytesRead = in.read(buffer)) > 0) { |
| 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 |
| response.setStatus(HttpServletResponse.SC_NO_CONTENT); |
| return; |
| } |
| |
| |
| // 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 != 2) { |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path"); |
| return; |
| } |
| |
| String uuid = splitPath[1]; |
| |
| 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); |
| } |
| |
| 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"); |
| } |
| } |