Skip to content

Commit

Permalink
Merge pull request #32 from delphi-hub/Feature/27/NativeExe
Browse files Browse the repository at this point in the history
Feature/27/native exe
  • Loading branch information
bhermann authored Mar 9, 2019
2 parents 6ee305e + 7d8068a commit 1396373
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 229 deletions.
23 changes: 21 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ libraryDependencies += "de.vandermeer" % "asciitable" % "0.3.2"
libraryDependencies += "com.lihaoyi" %% "fansi" % "0.2.5"
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
libraryDependencies += "au.com.bytecode" % "opencsv" % "2.4"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.4" % "test"

libraryDependencies ++= Seq(
"com.softwaremill.sttp" %% "core" % "1.5.4",
"com.softwaremill.sttp" %% "spray-json" % "1.5.4"
)


debianPackageDependencies := Seq("java8-runtime-headless")

Expand All @@ -38,11 +45,23 @@ lazy val cli = (project in file(".")).
enablePlugins(BuildInfoPlugin).
enablePlugins(DebianPlugin).
enablePlugins(WindowsPlugin).

enablePlugins(GraalVMNativeImagePlugin).
settings(
graalVMNativeImageOptions ++= Seq(
"--enable-https",
"--enable-http",
"--enable-all-security-services",
"--allow-incomplete-classpath",
"--enable-url-protocols=http,https"
)
).
enablePlugins(JDKPackagerPlugin).
settings(
buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),
buildInfoPackage := "de.upb.cs.swt.delphi.cli"
buildInfoPackage := "de.upb.cs.swt.delphi.cli",
)
scalastyleConfig := baseDirectory.value / "project" / "scalastyle-config.xml"

trapExit := false
fork := true
connectInput := true
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version = 1.1.1
sbt.version = 1.2.8
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// build management and packaging
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.15")

// coverage
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/de/upb/cs/swt/delphi/cli/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ package de.upb.cs.swt.delphi.cli
* @param verbose Marker if logging should be verbose
* @param mode The command to be run
*/
case class Config(server: String = sys.env.getOrElse("DELPHI_SERVER", "https://delphi.cs.uni-paderborn.de/api/"),
case class Config(server: String = sys.env.getOrElse("DELPHI_SERVER", "https://delphi.cs.uni-paderborn.de/api"),
verbose: Boolean = false,
raw: Boolean = false,
csv: String = "",
Expand Down
116 changes: 61 additions & 55 deletions src/main/scala/de/upb/cs/swt/delphi/cli/DelphiCLI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,81 +16,87 @@

package de.upb.cs.swt.delphi.cli

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import de.upb.cs.swt.delphi.cli.commands.{RetrieveCommand, SearchCommand, TestCommand}

import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext}

import com.softwaremill.sttp._
import de.upb.cs.swt.delphi.cli.commands._

/**
* The application class for the Delphi command line interface
*/
object DelphiCLI extends App {
object DelphiCLI {


def main(args: Array[String]): Unit = {

implicit val system = ActorSystem()
def getEnvOrElse(envVar: String, defaultPath: String) = sys.env.getOrElse(envVar, defaultPath)

val cliParser = {
new scopt.OptionParser[Config]("delphi-cli") {
head("Delphi Command Line Tool", s"(${BuildInfo.version})")
val javaLibPath = getEnvOrElse("JAVA_LIB_PATH", "/usr/lib/jvm/default-java/lib/")

version("version").text("Prints the version of the command line tool.")
val trustStorePath = getEnvOrElse("JAVA_TRUSTSTORE", "/usr/lib/jvm/default-java/lib/security/cacerts")

help("help").text("Prints this help text.")
override def showUsageOnError = true
System.setProperty("java.library.path", javaLibPath)
System.setProperty("javax.net.ssl.trustStore", trustStorePath)

opt[String]("server").action( (x,c) => c.copy(server = x)).text("The url to the Delphi server")
opt[Unit] (name = "raw").action((_,c) => c.copy(raw = true)).text("Output the raw results")
opt[Unit] (name = "silent").action((_,c) => c.copy(silent = true)).text("Suppress non-result output")
cliParser.parse(args, Config()) match {
case Some(c) =>

checkConfig(c => if (c.server.isEmpty()) failure("Option server is required.") else success)

cmd("test").action((_,c) => c.copy(mode = "test"))
implicit val config: Config = c
implicit val backend: SttpBackend[Id, Nothing] = HttpURLConnectionBackend()

cmd("retrieve").action((s,c) => c.copy(mode = "retrieve"))
.text("Retrieve a project's description, specified by ID.")
.children(
arg[String]("id").action((x, c) => c.copy(id = x)).text("The ID of the project to retrieve"),
opt[String]("csv").action((x, c) => c.copy(csv = x)).text("Path to the output .csv file (overwrites existing file)"),
opt[Unit]('f', "file").action((_, c) => c.copy(opts = List("file"))).text("Use to load the ID from file, " +
"with the filepath given in place of the ID")
)
if (!config.silent) cliParser.showHeader()

cmd("search").action((s, c) => c.copy(mode = "search"))
.text("Search artifact using a query.")
.children(
arg[String]("query").action((x,c) => c.copy(query = x)).text("The query to be used."),
opt[String]("csv").action((x, c) => c.copy(csv = x)).text("Path to the output .csv file (overwrites existing file)"),
opt[Int]("limit").action((x, c) => c.copy(limit = Some(x))).text("The maximal number of results returned."),
opt[Unit](name="list").action((_, c) => c.copy(list = true)).text("Output results as list (raw option overrides this)"),
opt[Int]("timeout").action((x, c) => c.copy(timeout = Some(x))).text("Timeout in seconds.")
)
config.mode match {
case "test" => TestCommand.execute
case "retrieve" => RetrieveCommand.execute
case "search" => SearchCommand.execute
case x => config.consoleOutput.outputError(s"Unknown command: $x")
}


case None =>
}

}

private def cliParser = {
val parser = {
new scopt.OptionParser[Config]("delphi-cli") {
head("Delphi Command Line Tool", s"(${BuildInfo.version})")

cliParser.parse(args, Config()) match {
case Some(config) =>
if (!config.silent) cliParser.showHeader()
config.mode match {
case "test" => TestCommand.execute(config)
case "retrieve" => RetrieveCommand.execute(config)
case "search" => SearchCommand.execute(config)
case x => config.consoleOutput.outputError(s"Unknown command: $x")
}
version("version").text("Prints the version of the command line tool.")

case None =>
}
help("help").text("Prints this help text.")

override def showUsageOnError = true

opt[String]("server").action((x, c) => c.copy(server = x)).text("The url to the Delphi server")
opt[Unit](name = "raw").action((_, c) => c.copy(raw = true)).text("Output the raw results")
opt[Unit](name = "silent").action((_, c) => c.copy(silent = true)).text("Suppress non-result output")

val poolShutdown = Http().shutdownAllConnectionPools()
Await.result(poolShutdown, Duration.Inf)
checkConfig(c => if (c.server.isEmpty()) failure("Option server is required.") else success)

implicit val ec: ExecutionContext = system.dispatcher
val terminationFuture = system.terminate()
cmd("test").action((_, c) => c.copy(mode = "test"))

terminationFuture.onComplete {
sys.exit(0)
cmd("retrieve").action((s, c) => c.copy(mode = "retrieve"))
.text("Retrieve a project's description, specified by ID.")
.children(
arg[String]("id").action((x, c) => c.copy(id = x)).text("The ID of the project to retrieve"),
opt[String]("csv").action((x, c) => c.copy(csv = x)).text("Path to the output .csv file (overwrites existing file)"),
opt[Unit]('f', "file").action((_, c) => c.copy(opts = List("file"))).text("Use to load the ID from file, " +
"with the filepath given in place of the ID")
)

cmd("search").action((s, c) => c.copy(mode = "search"))
.text("Search artifact using a query.")
.children(
arg[String]("query").action((x, c) => c.copy(query = x)).text("The query to be used."),
opt[String]("csv").action((x, c) => c.copy(csv = x)).text("Path to the output .csv file (overwrites existing file)"),
opt[Int]("limit").action((x, c) => c.copy(limit = Some(x))).text("The maximal number of results returned."),
opt[Unit](name = "list").action((_, c) => c.copy(list = true)).text("Output results as list (raw option overrides this)"),
opt[Int]("timeout").action((x, c) => c.copy(timeout = Some(x))).text("Timeout in seconds.")
)
}
}
parser
}
}
}
69 changes: 27 additions & 42 deletions src/main/scala/de/upb/cs/swt/delphi/cli/commands/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,68 +16,53 @@

package de.upb.cs.swt.delphi.cli.commands

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.Uri.Query
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, StatusCodes, Uri}
import akka.stream.ActorMaterializer
import akka.util.ByteString
import com.softwaremill.sttp._
import de.upb.cs.swt.delphi.cli.Config

import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
import scala.util.{Failure, Success}

/**
* Represents the implementation of a command of the CLI
*/
trait Command {

/**
* Executes the command implementation
*
* @param config The current configuration for the command
*/
def execute(config: Config)(implicit system : ActorSystem): Unit
def execute(implicit config: Config, backend: SttpBackend[Id, Nothing]): Unit = {}


/**
* Implements a common request type using currying to avoid code duplication
* @param target The endpoint to perform a Get request on
* @param config The current configuration for the command
* Http GET request template
*
* @param target Sub url in delphi server
* @param parameters Query params
* @return GET response
*/
protected def executeGet(target: String, parameters: Map[String, String] = Map())(config: Config, system : ActorSystem) : Option[String] = {
implicit val sys : ActorSystem = system
implicit val materializer = ActorMaterializer()
implicit val executionContext = sys.dispatcher

val uri = Uri(config.server)
config.consoleOutput.outputInformation(s"Contacting server ${uri}...")

val responseFuture = Http().singleRequest(HttpRequest(uri = uri.withPath(uri.path + target).withQuery(Query(parameters))))

responseFuture.onComplete {
case Failure(_) => error(config)(s"Could not reach server ${config.server}.")
case _ =>
protected def executeGet(paths: Seq[String], parameters: Map[String, String] = Map())
(implicit config: Config, backend: SttpBackend[Id, Nothing]): Option[String] = {
val serverUrl = uri"${config.server}"
val oldPath = serverUrl.path
val reqUrl = serverUrl.path(oldPath ++ paths).params(parameters)
val request = sttp.get(reqUrl)
config.consoleOutput.outputInformation(s"Sending request ${request.uri}")
val response = request.send()
response.body match {
case Left(value) =>
error.apply(s"Request failed:\n $value")
None
case Right(value) =>
Some(value)
}

val result = Await.result(responseFuture, 30 seconds)
val resultString = result match {
case HttpResponse(StatusCodes.OK, headers, entity, _) =>
entity.dataBytes.runFold(ByteString(""))(_ ++ _).map { body =>
Some(body.utf8String)
}
case resp @ HttpResponse(code, _, _, _) => {
error(config)("Artifact not found.")
resp.discardEntityBytes()
Future(None)
}
}

Await.result(resultString, Duration.Inf)
}


protected def information(implicit config: Config): String => Unit = config.consoleOutput.outputInformation _

protected def reportResult(implicit config: Config): Any => Unit = config.consoleOutput.outputResult _

protected def error(implicit config: Config): String => Unit = config.consoleOutput.outputError _

protected def success(implicit config: Config): String => Unit = config.consoleOutput.outputSuccess _

protected def exportResult(implicit config: Config): Any => Unit = config.csvOutput.exportResult _
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,22 @@

package de.upb.cs.swt.delphi.cli.commands

import akka.actor.ActorSystem
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import de.upb.cs.swt.delphi.cli.Config
import com.softwaremill.sttp.{Id, SttpBackend}
import de.upb.cs.swt.delphi.cli._
import de.upb.cs.swt.delphi.cli.artifacts.RetrieveResult
import de.upb.cs.swt.delphi.cli.artifacts.SearchResultJson._
import de.upb.cs.swt.delphi.cli.commands.SearchCommand.information
import spray.json.DefaultJsonProtocol
import spray.json._

import scala.concurrent.Await
import scala.concurrent.duration.Duration
import scala.io.Source
import scala.util.{Failure, Success}

/**
* The implementation of the retrieve command.
* Retrieves the contents of the file at the endpoint specified by the config file, and prints them to stdout
*/
object RetrieveCommand extends Command with SprayJsonSupport with DefaultJsonProtocol {
object RetrieveCommand extends Command {


override def execute(config: Config)(implicit system: ActorSystem): Unit = {
implicit val ec = system.dispatcher
implicit val materializer = ActorMaterializer()
override def execute(implicit config: Config, backend: SttpBackend[Id, Nothing]): Unit = {

//Checks whether the ID should be loaded from a file or not, and either returns the first line
// of the given file if it is, or the specified ID otherwise
Expand All @@ -56,35 +47,27 @@ object RetrieveCommand extends Command with SprayJsonSupport with DefaultJsonPro
}

val result = executeGet(
s"/retrieve/$checkTarget",
Seq("retrieve", checkTarget),
Map("pretty" -> "")
)(config, system)
)

result.map(s => {
result.foreach(s => {
if (config.raw) {
reportResult(config)(s)
reportResult.apply(s)
}

if (!config.raw || !config.csv.equals("")) {
val unmarshalledFuture = Unmarshal(s).to[List[RetrieveResult]]

unmarshalledFuture.transform {
case Success(unmarshalled) => {
val unmarshalled = Await.result(unmarshalledFuture, Duration.Inf)
success(config)(s"Found ${unmarshalled.size} item(s).")
reportResult(config)(unmarshalled)
//TODO: Direct convertTo[List[RetrieveResult]] not working ???


if(!config.csv.equals("")) {
exportResult(config)(unmarshalled)
information(config)("Results written to file '" + config.csv + "'")
}
val jsonArr = s.parseJson.asInstanceOf[JsArray].elements
val retrieveResults = jsonArr.map(r => r.convertTo[RetrieveResult])

Success(unmarshalled)
}
case Failure(e) => {
error(config)(s)
Failure(e)
}
success.apply(s"Found ${retrieveResults.size} item(s).")
reportResult.apply(retrieveResults)
if (!config.csv.equals("")) {
exportResult.apply(retrieveResults)
information.apply("Results written to file '" + config.csv + "'")
}
}
})
Expand Down
Loading

0 comments on commit 1396373

Please sign in to comment.