Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/27/native exe #32

Merged
merged 7 commits into from
Mar 9, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
scalaVersion := "2.12.4"

name := "delphi"
name := "delphi-cli"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you intend to rename this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The executable generated will take this name. So I have renamed it. I can revert if it is a issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's why i actually renamed it. :)
This way the users can enter delphi and not delphi-cli.
I think this ways its a bit stronger. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok then revert it to delphi. In documentation delphi-cli is used I renamed because of that https://github.com/delphi-hub/delphi-cli.

version := "1.0.0-SNAPSHOT"
maintainer := "Ben Hermann <[email protected]>"

Expand Down 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