diff --git a/README_SETUP.md b/README_SETUP.md index 34d6c77f4eb32192465f4620658089f406836a64..426867f6e9c51f74104ac1c769ace12a20f9d50c 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -10,6 +10,12 @@ * >=Screen 4.01 * >=MySQL 5.5 (better >=5.6) or >=MariaDB 10.1 +### Install the requirements on Debian +On Debian, the required packages can be installed with: + + apt-get install git make mariadb-server maven openjdk-8-jdk-headless \ + python3-pip screen + ## System * >=Linux 4.0.0, x86\_64, e.g. Ubuntu 14.04.1 diff --git a/conf/cache.ccf b/conf/cache.ccf new file mode 100644 index 0000000000000000000000000000000000000000..87d1338a5cf70da9b551744bc154190965d4bb46 --- /dev/null +++ b/conf/cache.ccf @@ -0,0 +1,3 @@ +jcs.default.cacheattributes=org.apache.commons.jcs.engine.CompositeCacheAttributes +jcs.default.cacheattributes.MaxObjects=1000 +jcs.default.cacheattributes.MemoryCacheName=org.apache.commons.jcs.engine.memory.lru.LRUMemoryCache \ No newline at end of file diff --git a/conf/logging.conf b/conf/logging.conf new file mode 100644 index 0000000000000000000000000000000000000000..c4ddcf78b8956daeb71768674812fc61ff3d8d19 --- /dev/null +++ b/conf/logging.conf @@ -0,0 +1,5 @@ +org.restlet.handlers = java.util.logging.ConsoleHandler +caosdb.server.handlers = caosdb.server.logging.BackendLoggingHandler, java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = INFO +caosdb.server.logging.BackendLogging.Handler.level = INFO \ No newline at end of file diff --git a/src/main/java/caosdb/server/CaosDBException.java b/src/main/java/caosdb/server/CaosDBException.java index adfca3d06f83d990ac540318138093cd0485cf7b..3e0e50769cd3f08423ea83fa851b2cdfca54e50f 100644 --- a/src/main/java/caosdb/server/CaosDBException.java +++ b/src/main/java/caosdb/server/CaosDBException.java @@ -22,8 +22,8 @@ */ package caosdb.server; -public class CaosDBException extends Exception { - /** */ +public class CaosDBException extends RuntimeException { + private static final long serialVersionUID = 5317733089121727021L; public CaosDBException(final String string) { diff --git a/src/main/java/caosdb/server/CaosDBServer.java b/src/main/java/caosdb/server/CaosDBServer.java index 5f2d31409d831d2da39318286cdeedee1c7b3566..811137780cee9c83e787af92d0d9f50b67272ed0 100644 --- a/src/main/java/caosdb/server/CaosDBServer.java +++ b/src/main/java/caosdb/server/CaosDBServer.java @@ -1,22 +1,19 @@ /* - * ** header v3.0 - * This file is a part of the CaosDB Project. + * ** header v3.0 This file is a part of the CaosDB Project. * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2018 Research Group Biomedical Physics, Max-Planck-Institute for Dynamics and + * Self-Organization Göttingen * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see <https://www.gnu.org/licenses/>. * * ** end header */ @@ -48,6 +45,7 @@ import caosdb.server.resource.InfoResource; import caosdb.server.resource.LogoutResource; import caosdb.server.resource.PermissionRulesResource; import caosdb.server.resource.RolesResource; +import caosdb.server.resource.ScriptingResource; import caosdb.server.resource.ServerLogsResource; import caosdb.server.resource.TestCaseFileSystemResource; import caosdb.server.resource.TestCaseResource; @@ -533,6 +531,7 @@ public class CaosDBServer extends Application { // After authentication comes authorization... authenticator.setNext(authorizer); + protectedRouter.attach("/scripting", ScriptingResource.class); protectedRouter.attach("/Entities", EntityResource.class); protectedRouter.attach("/Entities/", EntityResource.class); protectedRouter.attach("/Entities/{specifier}", EntityResource.class); @@ -582,12 +581,10 @@ public class CaosDBServer extends Application { protectedRouter.attachDefault(DefaultResource.class); /* - * Dirty Hack - no clean solution found yet (except for patching the - * restlet framework). The logging handler causes a NullPointerException - * when the Template.match method logs a warning. This warning - * (seemingly) always means that the RequestUri cannot be matched - * because its to long and causes a StackOverflow. Therefore we want to - * generate an HTTP 414 error. + * Dirty Hack - no clean solution found yet (except for patching the restlet framework). The + * logging handler causes a NullPointerException when the Template.match method logs a warning. + * This warning (seemingly) always means that the RequestUri cannot be matched because its to + * long and causes a StackOverflow. Therefore we want to generate an HTTP 414 error. */ final Handler handler = @@ -697,6 +694,20 @@ public class CaosDBServer extends Application { public static boolean isDebugMode() { return DEBUG_MODE; } + + /** + * Set a server property to a new value. This might not have an immediate effect if classes did + * already read an older configuration and stick to that. + * + * @param key, the server property. + * @param value, the new value. + */ + public static void setProperty(String key, String value) { + if (SERVER_PROPERTIES == null) { + SERVER_PROPERTIES = ServerProperties.initServerProperties(); + } + SERVER_PROPERTIES.setProperty(key, value); + } } class CaosDBComponent extends Component { diff --git a/src/main/java/caosdb/server/ServerProperties.java b/src/main/java/caosdb/server/ServerProperties.java index 9a33d2281b152e4aa23ceef59c074d625e690f02..011c9a6f599b596895298b2b11b54f9d7351665c 100644 --- a/src/main/java/caosdb/server/ServerProperties.java +++ b/src/main/java/caosdb/server/ServerProperties.java @@ -113,6 +113,10 @@ public class ServerProperties extends Properties { public static final String KEY_SERVER_OWNER = "SERVER_OWNER"; + public static final String KEY_SERVER_SIDE_SCRIPTING_BIN_DIR = "SERVER_SIDE_SCRIPTING_BIN_DIR"; + public static final String KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR = + "SERVER_SIDE_SCRIPTING_WORKING_DIR"; + /** * This init_server_properties method reads the config file which contains key-value-pairs for * such variables like the user name of the database, the port the server will be listening on @@ -123,6 +127,9 @@ public class ServerProperties extends Properties { final String basepath = System.getProperty("user.dir"); serverProperties.setProperty(KEY_SERVER_OWNER, ""); serverProperties.setProperty(KEY_SERVER_NAME, "CaosDB Server"); + serverProperties.setProperty(KEY_SERVER_SIDE_SCRIPTING_BIN_DIR, basepath + "/scripting/bin/"); + serverProperties.setProperty( + KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR, basepath + "/scripting/working/"); serverProperties.setProperty(KEY_FILE_SYSTEM_ROOT, "CaosDBFileSystem/FileSystemRoot/"); serverProperties.setProperty(KEY_DROP_OFF_BOX, "CaosDBFileSystem/DropOffBox/"); serverProperties.setProperty(KEY_TMP_FILES, "CaosDBFileSystem/TMP/"); @@ -165,7 +172,7 @@ public class ServerProperties extends Properties { serverProperties.setProperty(KEY_ACTIVATION_TIMEOUT_MS, "604800000"); // 7days serverProperties.setProperty(KEY_MAIL_HANDLER_CLASS, "caosdb.server.utils.mail.ToFileHandler"); - serverProperties.setProperty(KEY_MAIL_TO_FILE_HANDLER_LOC, "./OUTBOX"); + serverProperties.setProperty(KEY_MAIL_TO_FILE_HANDLER_LOC, "./"); serverProperties.setProperty(KEY_ADMIN_NAME, "CaosDB Admin"); serverProperties.setProperty(KEY_ADMIN_EMAIL, ""); diff --git a/src/main/java/caosdb/server/entity/FileProperties.java b/src/main/java/caosdb/server/entity/FileProperties.java index e31e9f9e2a253d59dd8164ec8c9a7a1c223ce006..7263b9b2b5e14f51988fa13dd969cf47f941478c 100644 --- a/src/main/java/caosdb/server/entity/FileProperties.java +++ b/src/main/java/caosdb/server/entity/FileProperties.java @@ -217,7 +217,7 @@ public class FileProperties { } private static Undoable delete(final File file) - throws IOException, InterruptedException, CaosDBException { + throws IOException, InterruptedException { if (file.getAbsolutePath().startsWith(FileSystem.getBasepath())) { final Undoable d; final File parent = file.getParentFile(); diff --git a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java index fb8196841e2e0ffb33273add737e8cf26e2034c0..e390d3c55576e774a78e89f17de73ed950b78cf4 100644 --- a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java +++ b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java @@ -39,6 +39,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.NoSuchElementException; +import java.util.logging.Level; import org.apache.commons.fileupload.FileUploadException; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; @@ -255,6 +256,10 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { this.xslScript = s; } + protected JdomRepresentation ok(Element root) { + return ok(new Document(root)); + } + protected JdomRepresentation ok(final Document doc) { return new JdomRepresentation(doc, MediaType.TEXT_XML, " ", getReference(), getXSLScript()); } @@ -313,6 +318,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { } catch (final JDOMException e) { return noWellFormedNess(); } catch (final Throwable e) { + getLogger().log(Level.SEVERE, "UNKNOWN ERROR", e); return error(ServerMessages.UNKNOWN_ERROR(getSRID())); } } @@ -337,6 +343,14 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { return this.flags; } + protected Element generateRootElement(Element... elements) { + Element root = generateRootElement(); + for (Element e : elements) { + root.addContent(e); + } + return root; + } + public static class XMLParser { private final LinkedList<SAXBuilder> pool = new LinkedList<SAXBuilder>(); private final int max = 25; diff --git a/src/main/java/caosdb/server/resource/ScriptingResource.java b/src/main/java/caosdb/server/resource/ScriptingResource.java new file mode 100644 index 0000000000000000000000000000000000000000..4687b2c5ad241baddc6662c5fb043ac955839ff6 --- /dev/null +++ b/src/main/java/caosdb/server/resource/ScriptingResource.java @@ -0,0 +1,210 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.resource; + +import caosdb.server.FileSystem; +import caosdb.server.accessControl.Principal; +import caosdb.server.accessControl.SessionToken; +import caosdb.server.entity.FileProperties; +import caosdb.server.entity.Message; +import caosdb.server.scripting.CallerSerializer; +import caosdb.server.scripting.ServerSideScriptingCaller; +import caosdb.server.utils.Serializer; +import caosdb.server.utils.ServerMessages; +import caosdb.server.utils.Utils; +import com.ibm.icu.text.Collator; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.commons.fileupload.FileItemIterator; +import org.apache.commons.fileupload.FileItemStream; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.jdom2.Element; +import org.restlet.data.CharacterSet; +import org.restlet.data.Form; +import org.restlet.data.MediaType; +import org.restlet.data.Parameter; +import org.restlet.data.Status; +import org.restlet.engine.header.ContentType; +import org.restlet.ext.fileupload.RestletFileUpload; +import org.restlet.representation.Representation; + +public class ScriptingResource extends AbstractCaosDBServerResource { + + private ServerSideScriptingCaller caller; + private Collection<FileProperties> deleteFiles = new LinkedList<>(); + + @Override + public Logger getLogger() { + return Logger.getLogger(this.getClass().getName()); + } + + public Element generateRootElement(ServerSideScriptingCaller caller) { + Serializer<ServerSideScriptingCaller, Element> xmlSerializer = new CallerSerializer(); + Element callerElement = xmlSerializer.serialize(caller); + + return generateRootElement(callerElement); + } + + @Override + protected Representation httpPostInChildClass(Representation entity) throws Exception { + + MediaType mediaType = entity.getMediaType(); + try { + if (mediaType.equals(MediaType.MULTIPART_FORM_DATA, true)) { + handleMultiparts(entity); + } else if (mediaType.equals(MediaType.APPLICATION_WWW_FORM)) { + handleForm(new Form(entity)); + } else { + getResponse().setStatus(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE); + return null; + } + } catch (Message m) { + return error(m, Status.valueOf(m.getCode())); + } finally { + deleteTmpFiles(); + } + return ok(generateRootElement(this.caller)); + } + + private void deleteTmpFiles() { + for (FileProperties p : deleteFiles) { + try { + p.getFile().delete(); + } catch (Exception t) { + if (getLogger().isLoggable(Level.WARNING)) { + getLogger().warning("Could not delete tmp file: " + p.getPath() + "\nException: " + t.toString()); + } + } + } + } + + public int handleMultiparts(Representation entity) + throws FileUploadException, IOException, NoSuchAlgorithmException, Message { + final DiskFileItemFactory factory = new DiskFileItemFactory(); + factory.setSizeThreshold(1000240); + final RestletFileUpload upload = new RestletFileUpload(factory); + final FileItemIterator iter = upload.getItemIterator(entity); + + List<FileProperties> files = new ArrayList<>(); + Form form = new Form(); + + while (iter.hasNext()) { + FileItemStream item = iter.next(); + if (item.isFormField()) { + // put plain text form field into the form + CharacterSet characterSet = ContentType.readCharacterSet(item.getContentType()); + String value = + Utils.InputStream2String( + item.openStream(), + (characterSet != null ? characterSet.toString() : CharacterSet.UTF_8.toString())); + form.add(new Parameter(item.getFieldName(), value)); + } else { + // -> this is a file, store in tmp dir + final FileProperties file = FileSystem.upload(item, this.getSRID()); + deleteTmpFileAfterTermination(file); + file.setPath(item.getName()); + file.setTmpIdentifyer(item.getFieldName()); + files.add(file); + form.add( + new Parameter( + item.getFieldName(), + ServerSideScriptingCaller.UPLOAD_FILES_DIR + "/" + item.getName())); + } + } + return callScript(form, files); + } + + private void deleteTmpFileAfterTermination(FileProperties file) { + deleteFiles.add(file); + } + + public int handleForm(Form form) throws Message { + return callScript(form, null); + } + + public List<String> form2CommandLine(Form form) throws Message { + ArrayList<String> commandLine = new ArrayList<>(form.size()); + ArrayList<Parameter> positionalArgs = new ArrayList<>(form.size() - 1); + + commandLine.add(""); // will be replaced by the "call" value + + for (Parameter p : form) { + if (p.getName().startsWith("-p")) { + positionalArgs.add(p); + } else if (p.getName().startsWith("-O")) { + commandLine.add(String.format("--%s=%s", p.getName().substring(2), p.getValue())); + } else if (p.getName().equals("call")) { + commandLine.set(0, p.getValue()); + } + } + + // sort positional arguments. + positionalArgs.sort((o1, o2) -> Collator.getInstance().compare(o1.getName(), o2.getName())); + for (Parameter p : positionalArgs) { + commandLine.add(p.getValue()); + } + + if (commandLine.get(0).length() == 0) { + // first item still empty + throw ServerMessages.SERVER_SIDE_SCRIPT_MISSING_CALL; + } + return commandLine; + } + + public int callScript(Form form, List<FileProperties> files) throws Message { + List<String> commandLine = form2CommandLine(form); + Integer timeoutMs = Integer.valueOf(form.getFirstValue("timeout", "-1")); + return callScript(commandLine, timeoutMs, files); + } + + public int callScript(List<String> commandLine, Integer timeoutMs, List<FileProperties> files) + throws Message { + return callScript(commandLine, timeoutMs, files, generateAuthToken()); + } + + public Object generateAuthToken() { + return SessionToken.generate((Principal) getUser().getPrincipal(), null); + } + + public int callScript( + List<String> commandLine, Integer timeoutMs, List<FileProperties> files, Object authToken) + throws Message { + caller = + new ServerSideScriptingCaller( + commandLine.toArray(new String[commandLine.size()]), timeoutMs, files, authToken); + return caller.invoke(); + } + + @Override + protected Representation httpGetInChildClass() throws Exception { + getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); + return null; + } +} diff --git a/src/main/java/caosdb/server/scripting/CallerSerializer.java b/src/main/java/caosdb/server/scripting/CallerSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..1bd265650bed0d5de865cc91b29db3327b56174b --- /dev/null +++ b/src/main/java/caosdb/server/scripting/CallerSerializer.java @@ -0,0 +1,36 @@ +package caosdb.server.scripting; + +import caosdb.server.CaosDBException; +import caosdb.server.utils.Serializer; +import java.io.IOException; +import org.jdom2.Element; + +public class CallerSerializer implements Serializer<ServerSideScriptingCaller, Element> { + + @Override + public Element serialize(ServerSideScriptingCaller caller) { + Element command = new Element("call"); + command.setText(String.join(" ", caller.getCommandLine())); + + Element stdout = new Element("stdout"); + try { + stdout.addContent(caller.getStdOut()); + } catch (IOException e) { + throw new CaosDBException(e); + } + + Element stderr = new Element("stderr"); + try { + stderr.addContent(caller.getStdErr()); + } catch (IOException e) { + throw new CaosDBException(e); + } + + Element script = new Element("script"); + script.setAttribute("code", caller.getCode().toString()); + script.addContent(command); + script.addContent(stdout); + script.addContent(stderr); + return script; + } +} diff --git a/src/main/java/caosdb/server/scripting/ScriptingUtils.java b/src/main/java/caosdb/server/scripting/ScriptingUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cee65faf9b25d3b9bfe0b28cc508edf59626682c --- /dev/null +++ b/src/main/java/caosdb/server/scripting/ScriptingUtils.java @@ -0,0 +1,96 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.scripting; + +import caosdb.server.CaosDBServer; +import caosdb.server.ServerProperties; +import caosdb.server.entity.Message; +import caosdb.server.utils.ConfigurationException; +import caosdb.server.utils.ServerMessages; +import caosdb.server.utils.Utils; +import java.io.File; +import java.nio.file.Path; + +public class ScriptingUtils { + + private File bin; + private File working; + + public ScriptingUtils() { + this.bin = + new File( + CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR)); + this.working = + new File( + CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR)); + if (!bin.exists()) { + bin.mkdirs(); + } + if (!bin.isDirectory()) { + throw new ConfigurationException( + "The ServerProperty `" + + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR + + "` must point to a directory"); + } + + if (!working.exists()) { + working.mkdirs(); + } + if (!working.isDirectory()) { + throw new ConfigurationException( + "The ServerProperty `" + + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR + + "` must point to a directory"); + } + } + + public File getScriptFile(final String command) { + final Path script = bin.toPath().resolve(command); + return script.toFile(); + } + + public void checkScriptExists(final String command) throws Message { + if (!getScriptFile(command).exists()) { + throw ServerMessages.SERVER_SIDE_SCRIPT_DOES_NOT_EXIST; + } + } + + public void checkScriptExecutable(final String command) throws Message { + if (!getScriptFile(command).canExecute()) { + throw ServerMessages.SERVER_SIDE_SCRIPT_NOT_EXECUTABLE; + } + } + + public File getTmpWorkingDir() { + String uid = Utils.getUID(); + return working.toPath().resolve(uid).toFile(); + } + + public File getStdOutFile(File workingDir) { + return workingDir.toPath().resolve(".STDOUT").toFile(); + } + + public File getStdErrFile(File workingDir) { + return workingDir.toPath().resolve(".STDERR").toFile(); + } +} diff --git a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java new file mode 100644 index 0000000000000000000000000000000000000000..2455916ec6245f1e721908b6e91f65b230b60a3a --- /dev/null +++ b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java @@ -0,0 +1,237 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.scripting; + +import caosdb.server.CaosDBException; +import caosdb.server.entity.FileProperties; +import caosdb.server.entity.Message; +import caosdb.server.utils.ServerMessages; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.ProcessBuilder.Redirect; +import java.util.Collection; +import java.util.List; +import org.apache.commons.io.FileUtils; + +public class ServerSideScriptingCaller { + + public static final String UPLOAD_FILES_DIR = ".upload_files"; + public static final Integer STARTED = -1; + private final String[] commandLine; + private final int timeoutMs; + private ScriptingUtils utils; + private List<FileProperties> files; + private File workingDir; + private Object authToken; + private File stdOutFile; + private File stdErrFile; + private String stdErr = null; + private String stdOut; + private Integer code = null; + + public Integer getCode() { + return code; + } + + File getUploadFilesDir() { + return getTmpWorkingDir().toPath().resolve(UPLOAD_FILES_DIR).toFile(); + } + + public ServerSideScriptingCaller( + String[] commandLine, Integer timeoutMs, List<FileProperties> files, Object authToken) { + this(commandLine, timeoutMs, files, authToken, new ScriptingUtils()); + } + + public ServerSideScriptingCaller( + String[] commandLine, + Integer timeoutMs, + List<FileProperties> files, + Object authToken, + ScriptingUtils utils) { + this(commandLine, timeoutMs, files, authToken, utils, utils.getTmpWorkingDir()); + } + + public ServerSideScriptingCaller( + String[] commandLine, + Integer timeoutMs, + List<FileProperties> files, + Object authToken, + ScriptingUtils utils, + File workingDir) { + this.utils = utils; + this.files = files; + this.commandLine = commandLine; + this.timeoutMs = timeoutMs; + this.authToken = authToken; + this.workingDir = workingDir; + this.stdOutFile = utils.getStdOutFile(workingDir); + this.stdErrFile = utils.getStdErrFile(workingDir); + } + + public int invoke() throws Message { + try { + checkCommandLine(commandLine); + try { + createWorkingDir(); + putFilesInWorkingDir(files); + } catch (final Exception e) { + e.printStackTrace(); + throw ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR; + } + + try { + return callScript(); + } catch (TimeoutException e) { + throw ServerMessages.SERVER_SIDE_SCRIPT_TIMEOUT; + } catch (final Throwable e) { + e.printStackTrace(); + throw ServerMessages.SERVER_SIDE_SCRIPT_ERROR; + } + } finally { + cleanup(); + } + } + + void checkCommandLine(String[] commandLine) throws Message { + utils.checkScriptExists(commandLine[0]); + utils.checkScriptExecutable(commandLine[0]); + } + + void putFilesInWorkingDir(final Collection<FileProperties> files) + throws FileNotFoundException, CaosDBException { + if (files == null) { + return; + } + + // create files dir + if (!getUploadFilesDir().mkdir()) { + throw new FileNotFoundException("Could not crete the FILE_UPLOAD_DIR."); + } + for (final FileProperties f : files) { + if (f.getPath() == null || f.getPath().isEmpty()) { + throw new CaosDBException("The path must not be null or empty!"); + } + caosdb.server.utils.FileUtils.createSymlink( + getUploadFilesDir().toPath().resolve(f.getPath()).toFile(), f.getFile()); + } + } + + void createWorkingDir() throws Exception { + if (getTmpWorkingDir().exists()) { + throw new Exception("The working directory must be non-existing when the caller is invoked."); + } + + // create working dir + getTmpWorkingDir().mkdirs(); + } + + void cleanup() { + // catch exception and throw afterwards, + IOException e1 = null; + try { + // cache script outputs + getStdErr(); + getStdOut(); + } catch (final IOException e2) { + e1 = e2; + } + try { + deleteWorkingDir(getTmpWorkingDir()); + } catch (final IOException e2) { + if (e1 == null) { + e1 = e2; + } else { + e1.addSuppressed(e2); + } + } + if (e1 != null) throw new RuntimeException("Cleanup failed.", e1); + } + + void deleteWorkingDir(final File pwd) throws IOException { + if (pwd.exists()) FileUtils.forceDelete(pwd); + } + + String makeCallAbsolute(String call) { + return utils.getScriptFile(call).getAbsolutePath(); + } + + String[] injectAuthToken(String[] commandLine) { + String[] newCommandLine = new String[commandLine.length + 1]; + newCommandLine[0] = commandLine[0]; + newCommandLine[1] = "--auth-token=" + authToken.toString(); + for (int i = 2; i < newCommandLine.length; i++) { + newCommandLine[i] = commandLine[i - 1]; + } + return newCommandLine; + } + + int callScript() throws IOException, InterruptedException, TimeoutException { + String[] effectiveCommandLine = injectAuthToken(getCommandLine()); + effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]); + final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine); + pb.redirectOutput(Redirect.to(getStdOutFile())); + pb.redirectError(Redirect.to(getStdErrFile())); + pb.directory(getTmpWorkingDir()); + + code = STARTED; + final TimeoutProcess process = new TimeoutProcess(pb.start(), getTimeoutMs()); + + code = process.waitFor(); + return code; + } + + public File getStdOutFile() { + return stdOutFile; + } + + public File getStdErrFile() { + return stdErrFile; + } + + public String[] getCommandLine() { + return this.commandLine; + } + + File getTmpWorkingDir() { + return this.workingDir; + } + + int getTimeoutMs() { + return this.timeoutMs; + } + + public String getStdErr() throws IOException { + if (stdErr == null && getStdErrFile().exists()) { + stdErr = FileUtils.readFileToString(getStdErrFile(), "UTF-8"); + } + return stdErr; + } + + public String getStdOut() throws IOException { + if (stdOut == null && getStdOutFile().exists()) { + stdOut = FileUtils.readFileToString(getStdOutFile(), "UTF-8"); + } + return stdOut; + } +} diff --git a/src/main/java/caosdb/server/scripting/TimeoutException.java b/src/main/java/caosdb/server/scripting/TimeoutException.java new file mode 100644 index 0000000000000000000000000000000000000000..655cb6a7782c29a894607db0373a4baa7b98611e --- /dev/null +++ b/src/main/java/caosdb/server/scripting/TimeoutException.java @@ -0,0 +1,32 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.scripting; + +public class TimeoutException extends Exception { + + public TimeoutException(final String string) { + super(string); + } + + private static final long serialVersionUID = 4871406372815136756L; +} diff --git a/src/main/java/caosdb/server/scripting/TimeoutProcess.java b/src/main/java/caosdb/server/scripting/TimeoutProcess.java new file mode 100644 index 0000000000000000000000000000000000000000..f41a2663c6aae169b0ce28d4e14ab43135653237 --- /dev/null +++ b/src/main/java/caosdb/server/scripting/TimeoutProcess.java @@ -0,0 +1,83 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.scripting; + +import java.io.InputStream; + +public class TimeoutProcess { + + private final Process process; + private boolean wasTimeouted = false; + + public TimeoutProcess(final Process process, final int ms) { + this.process = process; + if (ms > 0) { + watchTimeout(this, ms).start(); + } + } + + public static Thread watchTimeout(final TimeoutProcess p, final int ms) { + return new Thread( + new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(ms); + } catch (final InterruptedException e) { + e.printStackTrace(); + } + p.timeout(); + } + }, + "Timeout " + p.toString()); + } + + protected void timeout() { + if (this.process.isAlive()) { + this.wasTimeouted = true; + this.process.destroyForcibly(); + } + } + + public boolean wasTimeouted() { + return this.wasTimeouted; + } + + public int waitFor() throws InterruptedException, TimeoutException { + int code = this.process.waitFor(); + if (wasTimeouted) { + throw new TimeoutException("The process was terminated due to a timeout."); + } else { + return code; + } + } + + public InputStream getErrorStream() { + return this.process.getErrorStream(); + } + + public int exitValue() { + return this.process.exitValue(); + } +} diff --git a/src/main/java/caosdb/server/utils/ConfigurationException.java b/src/main/java/caosdb/server/utils/ConfigurationException.java new file mode 100644 index 0000000000000000000000000000000000000000..74a741873b3d4e53f6d5af1dfcf2bc0bda1b125b --- /dev/null +++ b/src/main/java/caosdb/server/utils/ConfigurationException.java @@ -0,0 +1,32 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils; + +public class ConfigurationException extends RuntimeException { + + private static final long serialVersionUID = -8445574584694720914L; + + public ConfigurationException(String reason) { + super(reason); + } +} diff --git a/src/main/java/caosdb/server/utils/FileUtils.java b/src/main/java/caosdb/server/utils/FileUtils.java index 878c17e43ae18733301e8ba4d790c92e412fd7d9..ca62cc61eda93d954b466bc124748c95d33def3f 100644 --- a/src/main/java/caosdb/server/utils/FileUtils.java +++ b/src/main/java/caosdb/server/utils/FileUtils.java @@ -460,6 +460,9 @@ public class FileUtils { } public static File createSymlink(final File link, final File target) { + if (target == null) { + throw new NullPointerException("target was null."); + } if (!target.exists()) { throw new TransactionException("target does not exist."); } diff --git a/src/main/java/caosdb/server/utils/Serializer.java b/src/main/java/caosdb/server/utils/Serializer.java new file mode 100644 index 0000000000000000000000000000000000000000..5508e99388a9e4191fd524ccbfc3e30007f6759a --- /dev/null +++ b/src/main/java/caosdb/server/utils/Serializer.java @@ -0,0 +1,6 @@ +package caosdb.server.utils; + +public interface Serializer<T, S> { + + public S serialize(T object); +} diff --git a/src/main/java/caosdb/server/utils/ServerMessages.java b/src/main/java/caosdb/server/utils/ServerMessages.java index f24f28ee0e264ae9102fe7159e655830f3847d0b..5d4a3c3a45eecf329fbc5eb72f9324919cd9d264 100644 --- a/src/main/java/caosdb/server/utils/ServerMessages.java +++ b/src/main/java/caosdb/server/utils/ServerMessages.java @@ -1,22 +1,19 @@ /* - * ** header v3.0 - * This file is a part of the CaosDB Project. + * ** header v3.0 This file is a part of the CaosDB Project. * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2018 Research Group Biomedical Physics, Max-Planck-Institute for Dynamics and + * Self-Organization Göttingen * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see <https://www.gnu.org/licenses/>. * * ** end header */ @@ -399,4 +396,26 @@ public class ServerMessages { public static final Message CANNOT_PARSE_UNIT = new Message(MessageType.Error, 0, "This unit cannot be parsed."); + + public static final Message SERVER_SIDE_SCRIPT_DOES_NOT_EXIST = + new Message( + MessageType.Error, 404, "This server-side script does not exist. Did you install it?"); + + public static final Message SERVER_SIDE_SCRIPT_NOT_EXECUTABLE = + new Message(MessageType.Error, 400, "This server-side script is not executable."); + + public static final Message SERVER_SIDE_SCRIPT_ERROR = + new Message(MessageType.Error, 500, "The invocation of this server-side script failed."); + + public static final Message SERVER_SIDE_SCRIPT_SETUP_ERROR = + new Message( + MessageType.Error, + 500, + "The setup routine for the server-side script failed. This might indicate a misconfiguration of the server. Please contact the administrator."); + + public static final Message SERVER_SIDE_SCRIPT_TIMEOUT = + new Message(MessageType.Error, 400, "This server-side script did not finish in time."); + + public static final Message SERVER_SIDE_SCRIPT_MISSING_CALL = + new Message(MessageType.Error, 400, "You must specify the `call` field."); } diff --git a/src/main/java/caosdb/server/utils/Utils.java b/src/main/java/caosdb/server/utils/Utils.java index 6901a4190288f0b1c47269ccb644c60ce2a6fb22..10f3de6c573332ef176f5ca1b8823aea5e04b1b4 100644 --- a/src/main/java/caosdb/server/utils/Utils.java +++ b/src/main/java/caosdb/server/utils/Utils.java @@ -165,7 +165,11 @@ public class Utils { } public static String InputStream2String(final InputStream is) { - try (Scanner s = new Scanner(is)) { + return InputStream2String(is, "UTF-8"); + } + + public static String InputStream2String(final InputStream is, String charset) { + try (Scanner s = new Scanner(is, charset)) { s.useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } diff --git a/src/main/java/caosdb/server/utils/fsm/ActionNotAllowedException.java b/src/main/java/caosdb/server/utils/fsm/ActionNotAllowedException.java new file mode 100644 index 0000000000000000000000000000000000000000..eedd272c5d18fae84dd17b1f2eec0e24aa0730e9 --- /dev/null +++ b/src/main/java/caosdb/server/utils/fsm/ActionNotAllowedException.java @@ -0,0 +1,39 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +public class ActionNotAllowedException extends RuntimeException { + + private final String action; + + public ActionNotAllowedException(final String action) { + this.action = action; + } + + private static final long serialVersionUID = -7723481489788838572L; + + @Override + public String getMessage() { + return "The action `" + this.action + "` is not allowed in the current state."; + } +} diff --git a/src/main/java/caosdb/server/utils/fsm/FiniteStateMachine.java b/src/main/java/caosdb/server/utils/fsm/FiniteStateMachine.java new file mode 100644 index 0000000000000000000000000000000000000000..7439d95a1820c91e93290b805de4fa0bdb31c273 --- /dev/null +++ b/src/main/java/caosdb/server/utils/fsm/FiniteStateMachine.java @@ -0,0 +1,88 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +import java.util.List; +import java.util.Map; + +public abstract class FiniteStateMachine<S extends State, T extends Transition> { + + public FiniteStateMachine(final S initial, final Map<S, Map<T, S>> transitions) + throws StateNotReachableException { + this.currentState = initial; + this.transitions = transitions; + checkEveryStateReachable(); + } + + private void checkEveryStateReachable() throws StateNotReachableException { + for (final State s : getAllStates()) { + if (!s.equals(this.currentState) && !stateIsReachable(s)) { + throw new StateNotReachableException(s); + } + } + } + + private boolean stateIsReachable(final State s) { + for (final Map<T, S> map : this.transitions.values()) { + if (map.containsValue(s)) { + return true; + } + } + return false; + } + + private final Map<S, Map<T, S>> transitions; + private S currentState = null; + + public void trigger(final T t) throws TransitionNotAllowedException { + final S old = this.currentState; + this.currentState = getNextState(t); + onAfterTransition(old, t, this.currentState); + } + + S getNextState(final T t) throws TransitionNotAllowedException { + final Map<T, S> map = this.transitions.get(this.currentState); + if (map != null && map.containsKey(t)) { + return map.get(t); + } + throw new TransitionNotAllowedException(this.getCurrentState(), t); + } + + public S getCurrentState() { + return this.currentState; + } + + public List<? extends State> getAllStates() { + return this.currentState.getAllStates(); + } + + /** + * Override this method in subclasses. The method is called immediately after a transition + * finished. + * + * @param from + * @param transition + * @param to + */ + protected void onAfterTransition(final S from, final T transition, final S to) {} +} diff --git a/src/main/java/caosdb/server/utils/fsm/MissingImplementationException.java b/src/main/java/caosdb/server/utils/fsm/MissingImplementationException.java new file mode 100644 index 0000000000000000000000000000000000000000..761abf2fe7d8e45438ae06717d7de63ac56afb45 --- /dev/null +++ b/src/main/java/caosdb/server/utils/fsm/MissingImplementationException.java @@ -0,0 +1,32 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +public class MissingImplementationException extends Exception { + + public MissingImplementationException(final State s) { + super("The state `" + s.toString() + "` has no implementation."); + } + + private static final long serialVersionUID = -1844861392151564478L; +} diff --git a/src/main/java/caosdb/server/utils/fsm/State.java b/src/main/java/caosdb/server/utils/fsm/State.java new file mode 100644 index 0000000000000000000000000000000000000000..66073d4d6b2e092d24cf94a18d73216d461f53c9 --- /dev/null +++ b/src/main/java/caosdb/server/utils/fsm/State.java @@ -0,0 +1,30 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +import java.util.List; + +public interface State { + + public List<State> getAllStates(); +} diff --git a/src/main/java/caosdb/server/utils/fsm/StateNotReachableException.java b/src/main/java/caosdb/server/utils/fsm/StateNotReachableException.java new file mode 100644 index 0000000000000000000000000000000000000000..9dbafcd940d5d07bc89d0aa110a39224a760c340 --- /dev/null +++ b/src/main/java/caosdb/server/utils/fsm/StateNotReachableException.java @@ -0,0 +1,32 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +public class StateNotReachableException extends Exception { + + public StateNotReachableException(final State s) { + super("The state `" + s.toString() + "` is not reachable."); + } + + private static final long serialVersionUID = -162428821672960032L; +} diff --git a/src/main/java/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java b/src/main/java/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java new file mode 100644 index 0000000000000000000000000000000000000000..ad5d50dc49d9e0feca0586ccfdb620c26b5d483d --- /dev/null +++ b/src/main/java/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java @@ -0,0 +1,60 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +import java.util.Map; + +public class StrategyFiniteStateMachine<S extends State, T extends Transition, I> + extends FiniteStateMachine<S, T> { + + public StrategyFiniteStateMachine( + final S initial, final Map<S, I> stateImplementations, final Map<S, Map<T, S>> transitions) + throws MissingImplementationException, StateNotReachableException { + super(initial, transitions); + this.stateImplementations = stateImplementations; + checkImplementationsComplete(); + } + + /** + * Check if every state has it's implementation. + * + * @throws MissingImplementationException + */ + private void checkImplementationsComplete() throws MissingImplementationException { + for (final State s : getAllStates()) { + if (!this.stateImplementations.containsKey(s)) { + throw new MissingImplementationException(s); + } + } + } + + private final Map<S, I> stateImplementations; + + public I getImplementation() { + return getImplementation(getCurrentState()); + } + + public I getImplementation(final S state) { + return this.stateImplementations.get(state); + } +} diff --git a/src/main/java/caosdb/server/utils/fsm/Transition.java b/src/main/java/caosdb/server/utils/fsm/Transition.java new file mode 100644 index 0000000000000000000000000000000000000000..432adc17b9e1c664b95c0e85f4f6918793bf39ae --- /dev/null +++ b/src/main/java/caosdb/server/utils/fsm/Transition.java @@ -0,0 +1,25 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +public interface Transition {} diff --git a/src/main/java/caosdb/server/utils/fsm/TransitionNotAllowedException.java b/src/main/java/caosdb/server/utils/fsm/TransitionNotAllowedException.java new file mode 100644 index 0000000000000000000000000000000000000000..e5b56f7f55438b1b40ee153c65eeafc60e959448 --- /dev/null +++ b/src/main/java/caosdb/server/utils/fsm/TransitionNotAllowedException.java @@ -0,0 +1,37 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +public class TransitionNotAllowedException extends Exception { + + private static final long serialVersionUID = -7688041374190101361L; + + public TransitionNotAllowedException(final State state, final Transition transition) { + super( + "The transition `" + + transition.toString() + + "` is not allowed in state `" + + state.toString() + + "."); + } +} diff --git a/src/main/java/caosdb/server/utils/mail/ToFileHandler.java b/src/main/java/caosdb/server/utils/mail/ToFileHandler.java index 56491eadf0c6c36264261b674034a2da841cda29..8a7a012bc6e2f5b9dbf02ca9286c2c2c5ee3c6d5 100644 --- a/src/main/java/caosdb/server/utils/mail/ToFileHandler.java +++ b/src/main/java/caosdb/server/utils/mail/ToFileHandler.java @@ -39,7 +39,7 @@ public class ToFileHandler implements MailHandler { final String loc = CaosDBServer.getServerProperty(ServerProperties.KEY_MAIL_TO_FILE_HANDLER_LOC); File f = new File(loc); - if (f.isDirectory()) { + if ((f.exists() && f.isDirectory()) || !f.exists() && loc.endsWith("/")) { f = new File(f.getAbsolutePath() + "/OUTBOX"); } if (!f.exists()) { diff --git a/src/test/java/caosdb/CaosDBTestClass.java b/src/test/java/caosdb/CaosDBTestClass.java new file mode 100644 index 0000000000000000000000000000000000000000..6b894c75b097b4aa324a432b6338f84a0527f84c --- /dev/null +++ b/src/test/java/caosdb/CaosDBTestClass.java @@ -0,0 +1,43 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb; + +import java.io.File; +import java.io.IOException; +import org.apache.commons.io.FileUtils; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +public class CaosDBTestClass { + + public static final File TEST_DIR = new File(".TEST_DIR"); + + @BeforeClass + public static final void setupAll() throws IOException { + TEST_DIR.mkdirs(); + FileUtils.forceDeleteOnExit(TEST_DIR); + } + + @AfterClass + public static final void teardownAll() {} +} diff --git a/src/test/java/caosdb/server/Misc.java b/src/test/java/caosdb/server/Misc.java index 625213e9723ea536e737096cb73b01c70cbaea1e..7848a88f2dc9218d294103fab14c058327bcf4e9 100644 --- a/src/test/java/caosdb/server/Misc.java +++ b/src/test/java/caosdb/server/Misc.java @@ -31,7 +31,6 @@ import static org.junit.Assert.assertTrue; import caosdb.server.database.misc.TransactionBenchmark; import caosdb.server.jobs.core.CheckFileStorageConsistency; import caosdb.server.utils.CronJob; -import caosdb.server.utils.mail.Mail; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -120,7 +119,7 @@ public class Misc { assertEquals("SOMETEST", matcher.group(2)); assertNull(matcher.group(1)); assertFalse(matcher.find()); - + matcher = parseargs.matcher("-t 12000/ExperimentalData"); assertTrue(matcher.find()); assertEquals("-t 12000", matcher.group(0)); @@ -175,19 +174,6 @@ public class Misc { assertEquals("m", unitStr); } - @Test - public void testMail() { - final Mail mail = - new Mail( - "The Admin", - "test@example.com", - "The User", - "test2@example.com", - "Test", - "This is a Test"); - mail.send(); - } - @Test public void testBla() { String v = "3140m"; diff --git a/src/test/java/caosdb/server/resource/TestScriptingResource.java b/src/test/java/caosdb/server/resource/TestScriptingResource.java new file mode 100644 index 0000000000000000000000000000000000000000..3b035fe703409605c59a77df70421e7516230376 --- /dev/null +++ b/src/test/java/caosdb/server/resource/TestScriptingResource.java @@ -0,0 +1,94 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.resource; + +import static org.junit.Assert.assertEquals; + +import caosdb.server.entity.Message; +import java.io.IOException; +import java.util.Date; +import java.util.List; +import org.junit.Test; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.data.Form; +import org.restlet.data.MediaType; +import org.restlet.data.Method; +import org.restlet.data.Status; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +public class TestScriptingResource { + + ScriptingResource resource = + new ScriptingResource() { + @Override + public int callScript( + java.util.List<String> invokation, + Integer timeout_ms, + java.util.List<caosdb.server.entity.FileProperties> files, + Object authToken) + throws Message { + return -1; + }; + + @Override + public Object generateAuthToken() { + return ""; + } + }; + + @Test + public void testUnsupportedMediaType() { + Representation entity = new StringRepresentation("asdf"); + entity.setMediaType(MediaType.TEXT_ALL); + Request request = new Request(Method.POST, "../test", entity); + request.getAttributes().put("SRID", "asdf1234"); + request.setDate(new Date()); + resource.init(null, request, new Response(null)); + resource.handle(); + assertEquals(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE, resource.getResponse().getStatus()); + } + + @Test + public void testForm2invocation() throws Message { + Form form = + new Form( + "-Ooption=OPTION&call=CA%20LL&-Ooption2=OPTION2&-p=POS1&-p4=POS3&-p2=POS2&IGNORED"); + List<String> list = resource.form2CommandLine(form); + assertEquals("CA LL", list.get(0)); + assertEquals("--option=OPTION", list.get(1)); + assertEquals("--option2=OPTION2", list.get(2)); + assertEquals("POS1", list.get(3)); + assertEquals("POS2", list.get(4)); + assertEquals("POS3", list.get(5)); + assertEquals(6, list.size()); + } + + @Test + public void testHandleForm() throws Message, IOException { + Form form = + new Form("-Ooption=OPTION&call=CALL&-Ooption2=OPTION2&-p=POS1&-p4=POS3&-p2=POS2&IGNORED"); + assertEquals(-1, resource.handleForm(form)); + } +} diff --git a/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java b/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java new file mode 100644 index 0000000000000000000000000000000000000000..b0ba596438a7c463f5e66c6d56dda1ad664493de --- /dev/null +++ b/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java @@ -0,0 +1,560 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.scripting; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import caosdb.CaosDBTestClass; +import caosdb.server.CaosDBException; +import caosdb.server.database.exceptions.TransactionException; +import caosdb.server.entity.FileProperties; +import caosdb.server.entity.Message; +import caosdb.server.utils.ServerMessages; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import org.apache.commons.io.FileUtils; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class TestServerSideScriptingCaller extends CaosDBTestClass { + + public static File testFolder = + TEST_DIR.toPath().resolve("serverSideScriptingCallerTestFolder").toFile(); + public static File testFile = testFolder.toPath().resolve("TestFile").toFile(); + public static File testExecutable = testFolder.toPath().resolve("TestScript").toFile(); + + @BeforeClass + public static void setupTestFolder() throws IOException { + FileUtils.forceDeleteOnExit(testFolder); + FileUtils.forceDeleteOnExit(testFile); + FileUtils.forceDeleteOnExit(testExecutable); + + assertFalse(testFolder.exists()); + assertFalse(testFile.exists()); + assertFalse(testExecutable.exists()); + testFolder.mkdirs(); + testFile.createNewFile(); + FileUtils.write(testFile, "asdfhaskdjfhaskjdf", "UTF-8"); + + testExecutable.createNewFile(); + testExecutable.setExecutable(true); + } + + @AfterClass + public static void deleteTestFolder() throws IOException { + FileUtils.forceDelete(testFile); + FileUtils.forceDelete(testFolder); + assertFalse(testFolder.exists()); + assertFalse(testFile.exists()); + } + + public void testExeExit(final int code) throws IOException { + FileUtils.write(testExecutable, "exit " + code, "UTF-8"); + } + + public void testSleep(final int seconds) throws IOException { + FileUtils.write(testExecutable, "sleep " + seconds, "UTF-8"); + } + + public void testPrintArgsToStdErr() throws IOException { + FileUtils.write(testExecutable, "(>&2 echo \"$@\")", "UTF-8"); + } + + public void testPrintArgsToStdOut() throws IOException { + FileUtils.write(testExecutable, "echo \"$@\"", "UTF-8"); + } + + private File pwd; + + @Before + public void setupPWD() throws IOException { + this.pwd = testFolder.toPath().resolve("testPWD").toFile(); + FileUtils.forceDeleteOnExit(this.pwd); + assertFalse(this.pwd.exists()); + } + + @After + public void deletePWD() throws IOException { + if (this.pwd.exists()) { + FileUtils.forceDelete(this.pwd); + } + assertFalse(this.pwd.exists()); + } + + @Rule public ExpectedException exception = ExpectedException.none(); + + @Test + public void testCreateWorkingDirFailure() throws Exception { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd); + + this.exception.expect(Exception.class); + this.exception.expectMessage( + "The working directory must be non-existing when the caller is invoked."); + + this.pwd.mkdirs(); + caller.createWorkingDir(); + } + + @Test + public void testCreateWorkingDirOk() throws Exception { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd); + + caller.createWorkingDir(); + + // exists + assertTrue(this.pwd.exists()); + assertTrue(this.pwd.isDirectory()); + } + + /** + * Throw {@link FileNotFoundException} because PWD does not exist. + * + * @throws IOException + * @throws CaosDBException + */ + @Test() + public void testPutFilesInWorkingDirFailure1() throws IOException, CaosDBException { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd); + + this.exception.expect(FileNotFoundException.class); + this.exception.expectMessage("FILE_UPLOAD_DIR"); + + caller.putFilesInWorkingDir(new ArrayList<>()); + } + + /** + * Throw {@link CaosDBException} because tmpIdentifyer is null or empty. + * + * @throws FileNotFoundException + * @throws CaosDBException + */ + @Test + public void testPutFilesInWorkingDirFailure2() throws FileNotFoundException, CaosDBException { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd); + this.pwd.mkdirs(); + + final ArrayList<FileProperties> files = new ArrayList<>(); + files.add(new FileProperties(null, null, null)); + + this.exception.expect(CaosDBException.class); + this.exception.expectMessage("The path must not be null or empty!"); + + caller.putFilesInWorkingDir(files); + } + + /** + * Throw {@link NullPointerException} because file is null. + * + * @throws FileNotFoundException + * @throws CaosDBException + */ + @Test + public void testPutFilesInWorkingDirFailure3() throws FileNotFoundException, CaosDBException { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd); + this.pwd.mkdirs(); + + final ArrayList<FileProperties> files = new ArrayList<>(); + final FileProperties f = new FileProperties(null, null, null); + f.setTmpIdentifyer("a2s3d4f5"); + f.setPath("testfile"); + files.add(f); + + this.exception.expect(NullPointerException.class); + this.exception.expectMessage("target was null."); + + caller.putFilesInWorkingDir(files); + } + + /** + * Throw {@link TransactionException} because file does not exist. + * + * @throws FileNotFoundException + * @throws CaosDBException + */ + @Test + public void testPutFilesInWorkingDirFailure4() throws FileNotFoundException, CaosDBException { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd); + this.pwd.mkdirs(); + + final ArrayList<FileProperties> files = new ArrayList<>(); + final FileProperties f = new FileProperties(null, null, null); + f.setTmpIdentifyer("a2s3d4f5"); + f.setFile(new File("blablabla_non_existing")); + f.setPath("bla"); + files.add(f); + + this.exception.expect(TransactionException.class); + this.exception.expectMessage("target does not exist"); + + caller.putFilesInWorkingDir(files); + } + + /** + * putFilesInWorkingDir returns silently because files is null. + * + * @throws IOException + * @throws CaosDBException + */ + @Test() + public void testPutFilesInWorkingDirReturnSilently() throws IOException, CaosDBException { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd); + + caller.putFilesInWorkingDir(null); + } + + @Test + public void testPutFilesInWorkingDirOk() throws CaosDBException, IOException { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd); + this.pwd.mkdirs(); + + final ArrayList<FileProperties> files = new ArrayList<>(); + final FileProperties f = new FileProperties(null, null, null); + f.setTmpIdentifyer("a2s3d4f5"); + f.setFile(testFile); + f.setPath("testfile"); + files.add(f); + + caller.putFilesInWorkingDir(files); + + assertEquals(1, caller.getUploadFilesDir().listFiles().length); + final File file = caller.getUploadFilesDir().listFiles()[0]; + FileUtils.contentEquals(file, testFile); + } + + /** + * Throw Exception because Script returns with error code 1. + * + * @throws Exception + */ + @Test + public void testCallScriptFailure1() throws Exception { + final String[] cmd = {testExecutable.getAbsolutePath()}; + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(cmd, -1, null, "", new ScriptingUtils(), this.pwd); + + caller.createWorkingDir(); + assertTrue(caller.getTmpWorkingDir().exists()); + + testExeExit(1); + + assertEquals(1, caller.callScript()); + } + + @Test + public void testCallScriptOkSimple() throws Exception { + final String[] cmd = {testExecutable.getAbsolutePath()}; + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(cmd, -1, null, "", new ScriptingUtils(), this.pwd); + + caller.createWorkingDir(); + assertTrue(caller.getTmpWorkingDir().exists()); + + testExeExit(0); + + assertEquals(0, caller.callScript()); + } + + @Test + public void testWatchTimeout() throws InterruptedException { + final TimeoutProcess p = + new TimeoutProcess( + new Process() { + + int exit = 0; + + @Override + public int waitFor() throws InterruptedException { + return this.exit; + } + + @Override + public OutputStream getOutputStream() { + return null; + } + + @Override + public InputStream getInputStream() { + return null; + } + + @Override + public InputStream getErrorStream() { + return null; + } + + @Override + public int exitValue() { + return this.exit; + } + + @Override + public void destroy() { + this.exit = 1; + } + + @Override + public boolean isAlive() { + return this.exit == 0; + } + }, + -1); + + assertFalse(p.wasTimeouted()); + final Thread watchTimeout = TimeoutProcess.watchTimeout(p, 100); + watchTimeout.start(); + watchTimeout.join(); + assertEquals(1, p.exitValue()); + assertTrue(p.wasTimeouted()); + } + + @Test(timeout = 1000) + public void testTimeout() throws Exception { + final String[] cmd = {testExecutable.getAbsolutePath()}; + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(cmd, 500, null, "", new ScriptingUtils(), this.pwd); + + caller.createWorkingDir(); + assertTrue(caller.getTmpWorkingDir().exists()); + + testSleep(10); + + this.exception.expect(TimeoutException.class); + caller.callScript(); + } + + @Test + public void testCleanup() throws Exception { + final String[] cmd = {testExecutable.getAbsolutePath()}; + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(cmd, 500, null, null, new ScriptingUtils(), this.pwd); + + caller.createWorkingDir(); + assertTrue(caller.getTmpWorkingDir().exists()); + caller.cleanup(); + assertFalse(caller.getTmpWorkingDir().exists()); + } + + @Test + public void testInvokeWithErrorInCreateWorkingDir() throws Message { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller( + new String[] {""}, -1, null, null, new ScriptingUtils(), this.pwd) { + @Override + public void createWorkingDir() throws Exception { + throw new Exception(); + } + + @Override + public void putFilesInWorkingDir(final Collection<FileProperties> files) + throws FileNotFoundException, CaosDBException {} + + @Override + public int callScript() { + return 0; + } + + @Override + public void cleanup() {} + }; + this.exception.expect( + new BaseMatcher<Exception>() { + + @Override + public boolean matches(final Object item) { + return item == ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR; + } + + @Override + public void describeTo(final Description description) { + description.appendValue(ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR); + } + }); + caller.invoke(); + } + + @Test + public void testInvokeWithErrorInPutFilesInWorkingDir() throws Message { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller( + new String[] {""}, -1, null, "", new ScriptingUtils(), this.pwd) { + @Override + public void createWorkingDir() throws Exception {} + + @Override + public void putFilesInWorkingDir(final Collection<FileProperties> files) + throws FileNotFoundException, CaosDBException { + throw new CaosDBException(""); + } + + @Override + public int callScript() { + return 0; + } + + @Override + public void cleanup() {} + }; + this.exception.expect( + new BaseMatcher<Exception>() { + + @Override + public boolean matches(final Object item) { + return item == ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR; + } + + @Override + public void describeTo(final Description description) { + description.appendValue(ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR); + } + }); + caller.invoke(); + } + + @Test + public void testInvokeWithErrorInCallScript() throws Message { + final String[] cmd = {testExecutable.getAbsolutePath()}; + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(cmd, -1, null, null, new ScriptingUtils(), this.pwd) { + @Override + public void createWorkingDir() throws Exception {} + + @Override + public void putFilesInWorkingDir(final Collection<FileProperties> files) + throws FileNotFoundException, CaosDBException {} + + @Override + public int callScript() throws IOException { + throw new IOException(); + } + + @Override + public void cleanup() {} + }; + this.exception.expect( + new BaseMatcher<Exception>() { + + @Override + public boolean matches(final Object item) { + return item == ServerMessages.SERVER_SIDE_SCRIPT_ERROR; + } + + @Override + public void describeTo(final Description description) { + description.appendValue(ServerMessages.SERVER_SIDE_SCRIPT_ERROR); + } + }); + caller.invoke(); + } + + @Test + public void testInvokeWithErrorInCleanup() throws Message { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd) { + @Override + public void createWorkingDir() throws Exception {} + + @Override + public void putFilesInWorkingDir(final Collection<FileProperties> files) + throws FileNotFoundException, CaosDBException {} + + @Override + public int callScript() { + return 0; + } + + @Override + public void cleanup() { + super.cleanup(); + } + + @Override + public void deleteWorkingDir(File pwd) throws IOException { + throw new IOException(); + } + }; + this.exception.expect(RuntimeException.class); + this.exception.expectMessage("Cleanup failed."); + caller.invoke(); + } + + @Test + public void testPrintStdErr() throws Exception { + final String[] cmd = {testExecutable.getAbsolutePath(), "opt1", "opt2"}; + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(cmd, -1, null, "authToken", new ScriptingUtils(), this.pwd); + + caller.createWorkingDir(); + + testPrintArgsToStdErr(); + + caller.callScript(); + + assertEquals( + "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdErrFile())); + } + + @Test + public void testPrintStdOut() throws Exception { + final String[] cmd = {testExecutable.getAbsolutePath(), "opt1", "opt2"}; + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(cmd, -1, null, "authToken", new ScriptingUtils(), this.pwd); + + caller.createWorkingDir(); + + testPrintArgsToStdOut(); + + caller.callScript(); + + assertEquals( + "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdOutFile())); + } + + @Test + public void testCleanupNonExistingWorkingDir() { + final ServerSideScriptingCaller caller = + new ServerSideScriptingCaller(null, -1, null, "authToken", new ScriptingUtils(), this.pwd); + caller.cleanup(); + } +} diff --git a/src/test/java/caosdb/server/utils/fsm/TestFiniteStateMachine.java b/src/test/java/caosdb/server/utils/fsm/TestFiniteStateMachine.java new file mode 100644 index 0000000000000000000000000000000000000000..3849df7dc66f8f812c36b0a4b49dcb2774d034b3 --- /dev/null +++ b/src/test/java/caosdb/server/utils/fsm/TestFiniteStateMachine.java @@ -0,0 +1,90 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.Lists; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +class SimpleFiniteStateMachine extends FiniteStateMachine<State, Transition> { + + public SimpleFiniteStateMachine( + final State initial, final Map<State, Map<Transition, State>> transitions) + throws StateNotReachableException { + super(initial, transitions); + } +} + +enum TestState implements State { + State1, + State2, + State3; + + @Override + public List<State> getAllStates() { + return Lists.newArrayList(values()); + } +} + +enum TestTransition implements Transition { + toState2, + toState3 +} + +public class TestFiniteStateMachine { + + @Rule public ExpectedException exc = ExpectedException.none(); + + @Test + public void testTransitionNotAllowedException() + throws StateNotReachableException, TransitionNotAllowedException { + final Map<State, Map<Transition, State>> map = new HashMap<>(); + final HashMap<Transition, State> from1 = new HashMap<>(); + from1.put(TestTransition.toState2, TestState.State2); + from1.put(TestTransition.toState3, TestState.State3); + map.put(TestState.State1, from1); + + final SimpleFiniteStateMachine fsm = new SimpleFiniteStateMachine(TestState.State1, map); + assertEquals(TestState.State1, fsm.getCurrentState()); + fsm.trigger(TestTransition.toState2); + assertEquals(TestState.State2, fsm.getCurrentState()); + + // only 1->2 and from 1->3 is allowed. not 2->3 + this.exc.expect(TransitionNotAllowedException.class); + fsm.trigger(TestTransition.toState3); + } + + @Test + public void testStateNotReachable() throws StateNotReachableException { + final Map<State, Map<Transition, State>> empty = new HashMap<>(); + + this.exc.expect(StateNotReachableException.class); + new SimpleFiniteStateMachine(TestState.State1, empty); + } +} diff --git a/src/test/java/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java b/src/test/java/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java new file mode 100644 index 0000000000000000000000000000000000000000..8d383383dd4a896f0befa1b3c2b1854129377353 --- /dev/null +++ b/src/test/java/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java @@ -0,0 +1,50 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.fsm; + +import java.util.HashMap; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class TestStrategyFiniteStateMachine { + + @Rule public ExpectedException exc = ExpectedException.none(); + + @Test + public void testStateHasNoImplementation() + throws MissingImplementationException, StateNotReachableException { + final Map<State, Map<Transition, State>> map = new HashMap<>(); + final HashMap<Transition, State> from1 = new HashMap<>(); + from1.put(TestTransition.toState2, TestState.State2); + from1.put(TestTransition.toState3, TestState.State3); + map.put(TestState.State1, from1); + + final Map<State, Object> stateImplementations = new HashMap<>(); + + this.exc.expect(MissingImplementationException.class); + new StrategyFiniteStateMachine<State, Transition, Object>( + TestState.State1, stateImplementations, map); + } +} diff --git a/src/test/java/caosdb/server/utils/mail/TestMail.java b/src/test/java/caosdb/server/utils/mail/TestMail.java new file mode 100644 index 0000000000000000000000000000000000000000..fc8bc887e71b6d02c31e47fb7125a17f4ea662c3 --- /dev/null +++ b/src/test/java/caosdb/server/utils/mail/TestMail.java @@ -0,0 +1,52 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package caosdb.server.utils.mail; + +import caosdb.CaosDBTestClass; +import caosdb.server.CaosDBServer; +import caosdb.server.ServerProperties; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestMail extends CaosDBTestClass { + + @BeforeClass + public static void setupUp() { + // output mails to the test dir + CaosDBServer.setProperty( + ServerProperties.KEY_MAIL_TO_FILE_HANDLER_LOC, TEST_DIR.getAbsolutePath()); + } + + @Test + public void testMail() { + final Mail mail = + new Mail( + "The Admin", + "test@example.com", + "The User", + "test2@example.com", + "Test", + "This is a Test"); + mail.send(); + } +}