blob: 82c54e8baa6a3178ec19bdff6cc639d11826f69d [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.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");
}
}