From: jsimomaa Date: Thu, 20 Jul 2017 11:59:19 +0000 (+0300) Subject: SCL REST API server X-Git-Tag: v1.31.0~264^2~45 X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=commitdiff_plain;h=db00b51c6ab9c63883de134e669b5da5be0f1bd5;hp=23fa194c0cf85db6c435f342600d30ddc0f8c620 SCL REST API server refs #7374 Change-Id: I24cf990d717cdc6bb3fc5bc44d4afdd39aade864 --- diff --git a/bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/commands/CommandSessionWithModules.java b/bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/commands/CommandSessionWithModules.java index 5ca800a21..57858a5cf 100644 --- a/bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/commands/CommandSessionWithModules.java +++ b/bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/commands/CommandSessionWithModules.java @@ -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 index 000000000..eca7bdba8 --- /dev/null +++ b/bundles/org.simantics.scl.rest/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/bundles/org.simantics.scl.rest/.gitignore b/bundles/org.simantics.scl.rest/.gitignore new file mode 100644 index 000000000..7fb5d66b1 --- /dev/null +++ b/bundles/org.simantics.scl.rest/.gitignore @@ -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 index 000000000..ddf0f2c29 --- /dev/null +++ b/bundles/org.simantics.scl.rest/.project @@ -0,0 +1,28 @@ + + + org.simantics.scl.rest + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + 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 index 000000000..e92a3cf0c --- /dev/null +++ b/bundles/org.simantics.scl.rest/META-INF/MANIFEST.MF @@ -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 index 000000000..a4fd10d42 --- /dev/null +++ b/bundles/org.simantics.scl.rest/build.properties @@ -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 index 000000000..8ed1f7e96 --- /dev/null +++ b/bundles/org.simantics.scl.rest/scl/SCL/REST/Server.scl @@ -0,0 +1,3 @@ +importJava "org.simantics.scl.rest.SCLRESTServer" where + start :: String -> Integer -> () + stop :: () 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 index 000000000..4f90e6900 --- /dev/null +++ b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/Activator.java @@ -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 index 000000000..f307b66dc --- /dev/null +++ b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/AuthorizationFilter.java @@ -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 index 000000000..cb86ff861 --- /dev/null +++ b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLAPI.java @@ -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 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 index 000000000..4ded2476d --- /dev/null +++ b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLRESTAPI.java @@ -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 buildJSONResponse(Object... keyValues) { + if ((keyValues.length % 2) != 0) + throw new IllegalArgumentException("Invalid amount of arguments! " + Arrays.toString(keyValues)); + Map 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 index 000000000..0e642026e --- /dev/null +++ b/bundles/org.simantics.scl.rest/src/org/simantics/scl/rest/SCLRESTServer.java @@ -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; + } +}