public void print(String text) {
try {
responseWriter.write(text + "\n");
+ responseWriter.flush();
} catch (IOException e) {
CommandSessionWithModules.LOGGER.error("Writing reponse failed.", e);
}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+.settings
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.simantics.scl.rest</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Simantics SCL REST-Server
+Bundle-SymbolicName: org.simantics.scl.rest
+Bundle-Version: 1.0.0.qualifier
+Bundle-Activator: org.simantics.scl.rest.Activator
+Require-Bundle: org.eclipse.core.runtime,
+ org.glassfish.jersey.core.jersey-server,
+ javax.ws.rs-api,
+ org.simantics.scl.compiler,
+ org.simantics.scl.osgi,
+ org.eclipse.jetty.servlet,
+ org.glassfish.jersey.containers.jersey-container-servlet-core,
+ javax.servlet-api,
+ org.eclipse.jetty.server,
+ org.eclipse.jetty.util,
+ org.eclipse.jetty.io,
+ com.fasterxml.jackson.core.jackson-core;bundle-version="2.8.8",
+ com.fasterxml.jackson.core.jackson-annotations;bundle-version="2.8.0",
+ com.fasterxml.jackson.core.jackson-databind;bundle-version="2.8.8",
+ org.glassfish.jersey.media.jersey-media-json-jackson;bundle-version="2.25.1",
+ org.glassfish.jersey.media.jersey-media-multipart;bundle-version="2.25.1",
+ org.slf4j.api,
+ org.jvnet.mimepull;bundle-version="1.9.6"
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Bundle-ActivationPolicy: lazy
--- /dev/null
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+ .,\
+ scl/
--- /dev/null
+importJava "org.simantics.scl.rest.SCLRESTServer" where
+ start :: String -> Integer -> <Proc> ()
+ stop :: <Proc> ()
--- /dev/null
+package org.simantics.scl.rest;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+public class Activator implements BundleActivator {
+
+ private static BundleContext context;
+
+ static BundleContext getContext() {
+ return context;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
+ */
+ public void start(BundleContext bundleContext) throws Exception {
+ Activator.context = bundleContext;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
+ */
+ public void stop(BundleContext bundleContext) throws Exception {
+ SCLRESTServer.stop();
+ Activator.context = null;
+ }
+
+}
--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2013, 2016 Association for Decentralized
+ * Information Management in Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the THTH Simantics
+ * Division Member Component License which accompanies this
+ * distribution, and is available at
+ * http://www.simantics.org/legal/sdmcl-v10.html
+ *
+ * Contributors:
+ * Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.scl.rest;
+
+import java.io.IOException;
+
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+public class AuthorizationFilter implements ContainerRequestFilter {
+
+ private final String token;
+
+ public AuthorizationFilter(String token) {
+ this.token = token;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext requestContext) throws IOException {
+ // Get the HTTP Authorization header from the request
+ String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
+
+ // Check if the HTTP Authorization header is present and formatted correctly
+ if (authorizationHeader == null || !authorizationHeader.startsWith("SCLRESTServer-Bearer ")) {
+ throw new NotAuthorizedException("Authorization header must be provided");
+ }
+
+ // Extract the token from the HTTP Authorization header
+ String token = authorizationHeader.substring("SCLRESTServer-Bearer".length()).trim();
+ try {
+ // Validate the token
+ validateToken(token);
+ } catch (Exception e) {
+ requestContext.abortWith(Response.status(Status.UNAUTHORIZED).build());
+ }
+ }
+
+ private void validateToken(String token) throws Exception {
+ if (!this.token.equals(token)) {
+ throw new Exception("Wrong token!");
+ }
+ }
+
+}
--- /dev/null
+package org.simantics.scl.rest;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.simantics.scl.compiler.commands.CommandSessionWithModules;
+import org.simantics.scl.osgi.SCLOsgi;
+
+public class SCLAPI {
+
+ private static SCLAPI INSTANCE;
+
+ private ConcurrentHashMap<String, CommandSessionWithModules> commandSessions;
+
+ private SCLAPI() {
+ this.commandSessions = new ConcurrentHashMap<>();
+ }
+
+ public static SCLAPI getInstance() {
+ if (INSTANCE == null) {
+ synchronized (SCLAPI.class) {
+ if (INSTANCE == null) {
+ INSTANCE = new SCLAPI();
+ }
+ }
+ }
+ return INSTANCE;
+ }
+
+ public CommandSessionWithModules getOrCreateCommandSession(String sessionId) {
+ return commandSessions.computeIfAbsent(sessionId, key -> new CommandSessionWithModules(SCLOsgi.MODULE_REPOSITORY));
+ }
+
+ public void execute(String sessionId, Reader reader, Writer writer) {
+ CommandSessionWithModules session = commandSessions.get(sessionId);
+ if (session == null)
+ throw new IllegalArgumentException("CommandSession for sessionId " + sessionId + " does not exist!");
+ session.runCommands(reader, writer);
+ }
+
+ public void deleteCommandSession(String sessionId) {
+ commandSessions.computeIfPresent(sessionId, (key, session) -> {
+ // session could be flushed or closed here to release possible resources?
+ return null;
+ });
+ }
+
+ public Object variableValue(String sessionId, String variableName) {
+ CommandSessionWithModules session = commandSessions.get(sessionId);
+ if (session == null)
+ throw new IllegalArgumentException("CommandSession for sessionId " + sessionId + " does not exist!");
+ return session.getCommandSession().getVariableValue(variableName);
+ }
+
+ public String putModule(String sessionId, String moduleName, String moduleText) {
+ CommandSessionWithModules session = commandSessions.get(sessionId);
+ if (session == null)
+ throw new IllegalArgumentException("CommandSession for sessionId " + sessionId + " does not exist!");
+ return session.putModule(moduleName, moduleText);
+ }
+}
--- /dev/null
+package org.simantics.scl.rest;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.StreamingOutput;
+
+import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
+import org.glassfish.jersey.media.multipart.FormDataParam;
+
+@Path("SCLAPI")
+@Produces(MediaType.APPLICATION_JSON)
+public class SCLRESTAPI {
+
+ private SCLAPI sclAPI;
+
+ public SCLRESTAPI() {
+ sclAPI = SCLAPI.getInstance();
+ }
+
+ private static Map<String, Object> buildJSONResponse(Object... keyValues) {
+ if ((keyValues.length % 2) != 0)
+ throw new IllegalArgumentException("Invalid amount of arguments! " + Arrays.toString(keyValues));
+ Map<String, Object> results = new HashMap<>(keyValues.length / 2);
+ for (int i = 0; i < keyValues.length; i += 2) {
+ Object key = keyValues[i];
+ Object value = keyValues[i + 1];
+ if (!(key instanceof String))
+ throw new IllegalArgumentException("Key with index " + i + " is not String");
+ results.put((String) key, value);
+ }
+ return results;
+ }
+
+ @Path("/sessions")
+ @POST
+ public Response sessions() {
+ String sessionId = UUID.randomUUID().toString();
+ sclAPI.getOrCreateCommandSession(sessionId);
+ return Response.ok(buildJSONResponse("sessionId", sessionId)).build();
+ }
+
+ @Path("/sessions/{sessionId}/modules/{moduleName:.*}")
+ @PUT
+ public Response upload(@PathParam("sessionId") String sessionId, @PathParam("moduleName") String moduleName, @FormDataParam("file") InputStream inputStream, @FormDataParam("file") FormDataContentDisposition fileDetail) throws IOException {
+ String moduleText = getModuleText(inputStream);
+ String response = sclAPI.putModule(sessionId, moduleName, moduleText);
+ if (response == null)
+ return Response.ok().build();
+ else
+ return Response.status(422).entity(buildJSONResponse("response", response)).build();
+ }
+
+ private static String getModuleText(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int length;
+ while ((length = inputStream.read(buffer)) != -1)
+ result.write(buffer, 0, length);
+ return result.toString(StandardCharsets.UTF_8.name());
+ }
+
+ @Path("/sessions/{sessionId}/execute")
+ @POST
+ @Produces(MediaType.TEXT_PLAIN)
+ public Response execute(@PathParam("sessionId") String sessionId, @FormDataParam("command") String command) {
+ final Reader reader = new InputStreamReader(new ByteArrayInputStream(command.getBytes(StandardCharsets.UTF_8)));
+ return Response.ok((StreamingOutput) output -> {
+ try (Writer writer = new BufferedWriter(new OutputStreamWriter(output))) {
+ sclAPI.execute(sessionId, reader, writer);
+ writer.flush();
+ }
+ }).build();
+ }
+
+ @Path("/sessions/{sessionId}/variables/{variableName}")
+ @GET
+ public Response variableValue(@PathParam("sessionId") String sessionId, @PathParam("variableName") String variableName) {
+ Object value = sclAPI.variableValue(sessionId, variableName);
+ return Response.ok(buildJSONResponse("sessionId", sessionId, "variableName", variableName, "variableValue", value)).build();
+ }
+
+ @Path("/sessions/{sessionId}/close")
+ @POST
+ public Response sessions(@PathParam("sessionId") String sessionId) {
+ sclAPI.deleteCommandSession(sessionId);
+ return Response.ok().build();
+ }
+
+}
--- /dev/null
+package org.simantics.scl.rest;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SCLRESTServer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SCLRESTServer.class);
+
+ private static SCLRESTServer INSTANCE = null;
+ private static Server server;
+ private static ServiceServerThread serverThread;
+
+ private SCLRESTServer(String token, int preferablePort) {
+ ResourceConfig config = new ResourceConfig();
+ // JSON serialization/deserialization
+ config.register(JacksonFeature.class);
+ // File upload
+ config.register(MultiPartFeature.class);
+ // Actual API
+ config.register(SCLRESTAPI.class);
+ // Authorization
+ config.register(new AuthorizationFilter(token));
+
+ ServletHolder holder = new ServletHolder(new ServletContainer(config));
+
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(preferablePort);
+ connector.setHost("localhost");
+
+ server.setConnectors(new Connector[] { connector });
+
+ ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
+ context.addServlet(holder, "/*");
+ }
+
+ private static class ServiceServerThread extends Thread {
+
+ @Override
+ public void run() {
+ try {
+ server.start();
+ server.join();
+ } catch (Exception e) {
+ LOGGER.error("Could not start server ", e);
+ }
+ }
+ }
+
+ private static synchronized SCLRESTServer getInstance(String token, int port) {
+ try {
+ if (INSTANCE == null) {
+ INSTANCE = new SCLRESTServer(token, port);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Could not initialize SCL REST server", e);
+ }
+ return INSTANCE;
+ }
+
+ public static synchronized void start(String token, int port) throws Exception {
+ // Ensure that an instance is created
+ getInstance(token, port);
+ if (serverThread == null && server != null) {
+ serverThread = new ServiceServerThread();
+ serverThread.start();
+ }
+ }
+
+ public static synchronized void stop() throws Exception {
+ if (server != null)
+ server.stop();
+ serverThread = null;
+ }
+}