diff --git a/gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java b/gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java index f99e3a97..1707896e 100644 --- a/gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java +++ b/gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java @@ -30,6 +30,7 @@ import org.gorpipe.gor.cli.manager.ManagerCommand; import org.gorpipe.gor.cli.migrator.FolderMigratorCommand; import org.gorpipe.gor.cli.query.QueryCommand; +import org.gorpipe.gor.cli.server.TestServerCommand; import org.gorpipe.gor.cli.render.RenderCommand; import org.gorpipe.logging.GorLogbackUtil; import picocli.CommandLine; @@ -38,7 +39,7 @@ @CommandLine.Command(name="gor", version="version 1.0", description = "Command line interface for gor query language and processes.", - subcommands = {QueryCommand.class, FolderMigratorCommand.class}) + subcommands = {QueryCommand.class, FolderMigratorCommand.class, TestServerCommand.class}) public class GorCLI extends GorExecCLI implements Runnable { public static void main(String[] args) { GorLogbackUtil.initLog("gor"); diff --git a/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java b/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java new file mode 100644 index 00000000..3bdeea9f --- /dev/null +++ b/gorscripts/src/main/java/org/gorpipe/gor/cli/server/TestServerCommand.java @@ -0,0 +1,79 @@ +/* + * BEGIN_COPYRIGHT + * + * Copyright (C) 2011-2013 deCODE genetics Inc. + * Copyright (C) 2013-2019 WuXi NextCode Inc. + * All Rights Reserved. + * + * GORpipe is free software: you can redistribute it and/or modify + * it under the terms of the AFFERO GNU General Public License as published by + * the Free Software Foundation. + * + * GORpipe is distributed "AS-IS" AND WITHOUT ANY WARRANTY OF ANY KIND, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE. See + * the AFFERO GNU General Public License for the complete license terms. + * + * You should have received a copy of the AFFERO GNU General Public License + * along with GORpipe. If not, see + * + * END_COPYRIGHT + */ + +package org.gorpipe.gor.cli.server; + +import gorsat.process.GorTestServer; +import gorsat.process.PipeOptions; +import org.gorpipe.gor.cli.HelpOptions; +import org.gorpipe.gor.model.DbConnection; +import org.gorpipe.gor.session.ProjectContext; +import org.gorpipe.util.ConfigUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.io.File; + +@CommandLine.Command(name = "test-server", + description = "Start an HTTP server that accepts GOR queries via POST /query", + header = "Start a GOR test server") +public class TestServerCommand extends HelpOptions implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(TestServerCommand.class); + + @CommandLine.Option(names = {"-p", "--port"}, defaultValue = "4242", + description = "Port to listen on (default: 4242)") + private int port; + + @CommandLine.Option(names = {"-d", "--cachedir"}, + description = "Path to cache directory for query execution") + private String cacheDir; + + @CommandLine.Option(names = {"-w", "--workers"}, defaultValue = "0", + description = "Number of parallel workers for query execution") + private int workers; + + @CommandLine.Option(names = {"-c", "--config"}, + description = "Loads configuration from external file") + private File configFile; + + @Override + public void run() { + try { + ConfigUtil.loadConfig("gor"); + DbConnection.initInConsoleApp(); + + PipeOptions opts = new PipeOptions(); + opts.cacheDir_$eq(cacheDir != null ? cacheDir : ProjectContext.DEFAULT_CACHE_DIR); + opts.workers_$eq(workers); + opts.color_$eq("none"); + if (configFile != null) + opts.configFile_$eq(configFile.toString()); + + GorTestServer.start(port, opts); + } catch (Exception e) { + log.error("Failed to start test server: {}", e.getMessage(), e); + System.exit(-1); + } + } +} diff --git a/gortools/src/main/scala/gorsat/Outputs/ColorStdOut.scala b/gortools/src/main/scala/gorsat/Outputs/ColorStdOut.scala index fd810dac..22345ded 100644 --- a/gortools/src/main/scala/gorsat/Outputs/ColorStdOut.scala +++ b/gortools/src/main/scala/gorsat/Outputs/ColorStdOut.scala @@ -22,12 +22,12 @@ package gorsat.Outputs -import gorsat.Commands.RowHeader import gorsat.process.PipeInstance +import java.io.OutputStream import org.gorpipe.gor.model.{Row, RowColorize} -case class ColorStdOut(instance: PipeInstance = null, colorFormatter: RowColorize) - extends OutStream(null , System.out) { +case class ColorStdOut(instance: PipeInstance = null, colorFormatter: RowColorize, dest: OutputStream = System.out) + extends OutStream(null, dest) { var headerPrinted : Boolean = false diff --git a/gortools/src/main/scala/gorsat/Outputs/NorColorStdOut.scala b/gortools/src/main/scala/gorsat/Outputs/NorColorStdOut.scala index c319a5b0..5f791b6c 100644 --- a/gortools/src/main/scala/gorsat/Outputs/NorColorStdOut.scala +++ b/gortools/src/main/scala/gorsat/Outputs/NorColorStdOut.scala @@ -23,10 +23,11 @@ package gorsat.Outputs import gorsat.process.PipeInstance +import java.io.OutputStream import org.gorpipe.gor.model.{Row, RowColorize} -case class NorColorStdOut(instance: PipeInstance = null, colorFormatter: RowColorize) - extends NorOutStream(null, System.out) { +case class NorColorStdOut(instance: PipeInstance = null, colorFormatter: RowColorize, dest: OutputStream = System.out) + extends NorOutStream(null, dest) { var headerPrinted : Boolean = false diff --git a/gortools/src/main/scala/gorsat/Outputs/NorStdOut.scala b/gortools/src/main/scala/gorsat/Outputs/NorStdOut.scala index 401bd932..3c2f5243 100644 --- a/gortools/src/main/scala/gorsat/Outputs/NorStdOut.scala +++ b/gortools/src/main/scala/gorsat/Outputs/NorStdOut.scala @@ -22,5 +22,7 @@ package gorsat.Outputs -case class NorStdOut(override val header: String = null) extends NorOutStream(header, System.out) { +import java.io.OutputStream + +case class NorStdOut(override val header: String = null, dest: OutputStream = System.out) extends NorOutStream(header, dest) { } diff --git a/gortools/src/main/scala/gorsat/Outputs/StdOut.scala b/gortools/src/main/scala/gorsat/Outputs/StdOut.scala index 80a5c253..29b3c141 100644 --- a/gortools/src/main/scala/gorsat/Outputs/StdOut.scala +++ b/gortools/src/main/scala/gorsat/Outputs/StdOut.scala @@ -22,6 +22,8 @@ package gorsat.Outputs -case class StdOut( val header: String = null) extends OutStream (header, System.out) { +import java.io.OutputStream + +case class StdOut(val header: String = null, dest: OutputStream = System.out) extends OutStream(header, dest) { } \ No newline at end of file diff --git a/gortools/src/main/scala/gorsat/process/CLIGorExecutionEngine.scala b/gortools/src/main/scala/gorsat/process/CLIGorExecutionEngine.scala index 6c2907af..5cb8013f 100644 --- a/gortools/src/main/scala/gorsat/process/CLIGorExecutionEngine.scala +++ b/gortools/src/main/scala/gorsat/process/CLIGorExecutionEngine.scala @@ -31,6 +31,7 @@ import org.gorpipe.gor.RequestStats import org.gorpipe.gor.driver.meta.DataType import org.gorpipe.gor.util.DataUtil import org.gorpipe.gor.model.{RowRotatingColorize, RowTypeColorize} +import java.io.OutputStream /** * Execution engine for GOR running as command line. This class takes as input the command line options, construct a @@ -39,11 +40,16 @@ import org.gorpipe.gor.model.{RowRotatingColorize, RowTypeColorize} * @param pipeOptions GorPipe command line options * @param whitelistedCmdFiles File containing whitelisted commands * @param securityContext Security context if needed + * @param outputStream Stream to write query results to (defaults to stdout) */ -class CLIGorExecutionEngine(pipeOptions: PipeOptions, whitelistedCmdFiles:String = null, securityContext:String = null) extends GorExecutionEngine { +class CLIGorExecutionEngine(pipeOptions: PipeOptions, whitelistedCmdFiles:String, securityContext:String, outputStream: OutputStream) extends GorExecutionEngine { + + def this(pipeOptions: PipeOptions, whitelistedCmdFiles: String, securityContext: String) = { + this(pipeOptions, whitelistedCmdFiles, securityContext, System.out) + } def this(args:Array[String], whitelistedCmdFiles:String, securityContext:String) = { - this(PipeOptions.parseInputArguments(args), whitelistedCmdFiles, securityContext) + this(PipeOptions.parseInputArguments(args), whitelistedCmdFiles, securityContext, System.out) } override protected def createSession(): GorSession = { @@ -66,7 +72,7 @@ class CLIGorExecutionEngine(pipeOptions: PipeOptions, whitelistedCmdFiles:String if (MacroUtilities.isLastCommandWrite(pipeOptions.query)) instance = null iterator.thePipeStep = iterator.thePipeStep | - createStdOut(session.getNorContext || iterator.isNorContext, pipeOptions.color, iterator) + createStdOut(session.getNorContext || iterator.isNorContext, pipeOptions.color, iterator, outputStream) iterator } @@ -83,24 +89,24 @@ class CLIGorExecutionEngine(pipeOptions: PipeOptions, whitelistedCmdFiles:String } } - private def createStdOut(isNor: Boolean, color: String, iterator: PipeInstance): OutStream = { + private def createStdOut(isNor: Boolean, color: String, iterator: PipeInstance, out: OutputStream): OutStream = { val c = color.toLowerCase() if (isNor) { if (c.startsWith("r")) { - NorColorStdOut(iterator, new RowRotatingColorize()) + NorColorStdOut(iterator, new RowRotatingColorize(), out) } else if(c.startsWith("t")) { - NorColorStdOut(iterator, new RowTypeColorize()) + NorColorStdOut(iterator, new RowTypeColorize(), out) } else { - NorStdOut(if (iterator == null) null else iterator.getHeader()) + NorStdOut(if (iterator == null) null else iterator.getHeader(), out) } } else { if (c.startsWith("r")) { - ColorStdOut(iterator, new RowRotatingColorize()) + ColorStdOut(iterator, new RowRotatingColorize(), out) } else if (c.startsWith("t")) { - ColorStdOut(iterator, new RowTypeColorize()) + ColorStdOut(iterator, new RowTypeColorize(), out) } else { - StdOut(if (iterator == null) null else iterator.getHeader()) + StdOut(if (iterator == null) null else iterator.getHeader(), out) } } } diff --git a/gortools/src/main/scala/gorsat/process/GorPipe.scala b/gortools/src/main/scala/gorsat/process/GorPipe.scala index 75f67e72..b340ac13 100644 --- a/gortools/src/main/scala/gorsat/process/GorPipe.scala +++ b/gortools/src/main/scala/gorsat/process/GorPipe.scala @@ -80,7 +80,6 @@ object GorPipe extends GorPipeFirstOrderCommands { // Initialize database connections DbConnection.initInConsoleApp() - var exitCode = 0 //todo find a better way to construct diff --git a/gortools/src/main/scala/gorsat/process/GorTestServer.scala b/gortools/src/main/scala/gorsat/process/GorTestServer.scala new file mode 100644 index 00000000..16e41be3 --- /dev/null +++ b/gortools/src/main/scala/gorsat/process/GorTestServer.scala @@ -0,0 +1,86 @@ +/* + * BEGIN_COPYRIGHT + * + * Copyright (C) 2011-2013 deCODE genetics Inc. + * Copyright (C) 2013-2019 WuXi NextCode Inc. + * All Rights Reserved. + * + * GORpipe is free software: you can redistribute it and/or modify + * it under the terms of the AFFERO GNU General Public License as published by + * the Free Software Foundation. + * + * GORpipe is distributed "AS-IS" AND WITHOUT ANY WARRANTY OF ANY KIND, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE. See + * the AFFERO GNU General Public License for the complete license terms. + * + * You should have received a copy of the AFFERO GNU General Public License + * along with GORpipe. If not, see + * + * END_COPYRIGHT + */ + +package gorsat.process + +import com.sun.net.httpserver.{HttpExchange, HttpServer} +import java.io.ByteArrayOutputStream +import java.net.InetSocketAddress +import org.gorpipe.exceptions.{ExceptionUtilities, GorException} +import org.slf4j.LoggerFactory + +object GorTestServer { + + private val logger = LoggerFactory.getLogger(this.getClass) + + def start(port: Int, baseOptions: PipeOptions): Unit = { + val server = HttpServer.create(new InetSocketAddress(port), 0) + + server.createContext("/query", (exchange: HttpExchange) => { + if (exchange.getRequestMethod.equalsIgnoreCase("POST")) { + val query = new String(exchange.getRequestBody.readAllBytes(), "UTF-8").trim + val (status, body) = executeQuery(query, baseOptions) + val bytes = body.getBytes("UTF-8") + exchange.getResponseHeaders.set("Content-Type", "text/plain; charset=UTF-8") + exchange.sendResponseHeaders(status, bytes.length) + val os = exchange.getResponseBody + os.write(bytes) + os.close() + } else { + exchange.sendResponseHeaders(405, -1) + exchange.getResponseBody.close() + } + }) + + server.start() + System.err.println(s"GOR test server listening on port $port") + logger.info(s"GOR test server started on port $port") + + Thread.currentThread().join() + } + + private def executeQuery(query: String, baseOptions: PipeOptions): (Int, String) = { + val opts = new PipeOptions() + opts.query = PipeOptions.cleanUpQueryAndSplit(query).mkString(";") + opts.gorRoot = baseOptions.gorRoot + opts.configFile = baseOptions.configFile + opts.aliasFile = baseOptions.aliasFile + opts.cacheDir = baseOptions.cacheDir + opts.color = "none" + + val out = new ByteArrayOutputStream() + val engine = new CLIGorExecutionEngine(opts, null, null, out) + try { + engine.execute() + (200, out.toString("UTF-8")) + } catch { + case ge: GorException => + val msg = ExceptionUtilities.gorExceptionToString(ge) + logger.error("Query failed: {}", msg) + (500, msg) + case ex: Throwable => + val msg = Option(ex.getMessage).getOrElse(ex.getClass.getName) + logger.error("Query failed: {}", msg, ex) + (500, msg) + } + } +}