SCL REST API server 34/734/6
authorjsimomaa <jani.simomaa@gmail.com>
Thu, 20 Jul 2017 11:59:19 +0000 (14:59 +0300)
committerJani Simomaa <jani.simomaa@semantum.fi>
Thu, 20 Jul 2017 18:44:53 +0000 (21:44 +0300)
refs #7374

Change-Id: I24cf990d717cdc6bb3fc5bc44d4afdd39aade864

12 files changed:
bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/commands/CommandSessionWithModules.java
bundles/org.simantics.scl.rest/.classpath [new file with mode: 0644]
bundles/org.simantics.scl.rest/.gitignore [new file with mode: 0644]
bundles/org.simantics.scl.rest/.project [new file with mode: 0644]
bundles/org.simantics.scl.rest/META-INF/MANIFEST.MF [new file with mode: 0644]
bundles/org.simantics.scl.rest/build.properties [new file with mode: 0644]
bundles/org.simantics.scl.rest/scl/SCL/REST/Server.scl [new file with mode: 0644]
bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/Activator.java [new file with mode: 0644]
bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/AuthorizationFilter.java [new file with mode: 0644]
bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLAPI.java [new file with mode: 0644]
bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLRESTAPI.java [new file with mode: 0644]
bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLRESTServer.java [new file with mode: 0644]

index 5ca800a21d950f46c7f6b889aa074cf4bbf21d9a..57858a5cfec103a5b9cacb6635b5d444a8a9dedc 100644 (file)
@@ -78,6 +78,7 @@ public class CommandSessionWithModules {
             public void print(String text) {
                 try {
                     responseWriter.write(text + "\n");
+                    responseWriter.flush();
                 } catch (IOException e) {
                     CommandSessionWithModules.LOGGER.error("Writing reponse failed.", e);
                 }
diff --git a/bundles/org.simantics.scl.rest/.classpath b/bundles/org.simantics.scl.rest/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?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>
diff --git a/bundles/org.simantics.scl.rest/.gitignore b/bundles/org.simantics.scl.rest/.gitignore
new file mode 100644 (file)
index 0000000..7fb5d66
--- /dev/null
@@ -0,0 +1 @@
+.settings
\ No newline at end of file
diff --git a/bundles/org.simantics.scl.rest/.project b/bundles/org.simantics.scl.rest/.project
new file mode 100644 (file)
index 0000000..ddf0f2c
--- /dev/null
@@ -0,0 +1,28 @@
+<?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>
diff --git a/bundles/org.simantics.scl.rest/META-INF/MANIFEST.MF b/bundles/org.simantics.scl.rest/META-INF/MANIFEST.MF
new file mode 100644 (file)
index 0000000..e92a3cf
--- /dev/null
@@ -0,0 +1,26 @@
+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
diff --git a/bundles/org.simantics.scl.rest/build.properties b/bundles/org.simantics.scl.rest/build.properties
new file mode 100644 (file)
index 0000000..a4fd10d
--- /dev/null
@@ -0,0 +1,5 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               scl/
diff --git a/bundles/org.simantics.scl.rest/scl/SCL/REST/Server.scl b/bundles/org.simantics.scl.rest/scl/SCL/REST/Server.scl
new file mode 100644 (file)
index 0000000..8ed1f7e
--- /dev/null
@@ -0,0 +1,3 @@
+importJava "org.simantics.scl.rest.SCLRESTServer" where
+    start :: String -> Integer -> <Proc> ()
+    stop :: <Proc> ()
diff --git a/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/Activator.java b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/Activator.java
new file mode 100644 (file)
index 0000000..4f90e69
--- /dev/null
@@ -0,0 +1,35 @@
+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;
+    }
+
+}
diff --git a/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/AuthorizationFilter.java b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/AuthorizationFilter.java
new file mode 100644 (file)
index 0000000..f307b66
--- /dev/null
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * 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!");
+        }
+    }
+
+}
diff --git a/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLAPI.java b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLAPI.java
new file mode 100644 (file)
index 0000000..cb86ff8
--- /dev/null
@@ -0,0 +1,62 @@
+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);
+    }
+}
diff --git a/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLRESTAPI.java b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLRESTAPI.java
new file mode 100644 (file)
index 0000000..4ded247
--- /dev/null
@@ -0,0 +1,110 @@
+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();
+    }
+
+}
diff --git a/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLRESTServer.java b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLRESTServer.java
new file mode 100644 (file)
index 0000000..0e64202
--- /dev/null
@@ -0,0 +1,85 @@
+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;
+    }
+}