diff --git a/.travis.yml b/.travis.yml
index 0a2dc6c..7391247 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,9 @@
language: scala
scala:
- - 2.12.4
+ - 2.13.1
script:
- 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then sbt ++$TRAVIS_SCALA_VERSION test; fi'
- - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then sbt ++$TRAVIS_SCALA_VERSION coverage test coverageReport coverageAggregate codacyCoverage; fi'
+ - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then sbt ++$TRAVIS_SCALA_VERSION test; fi'
+# - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then sbt ++$TRAVIS_SCALA_VERSION coverage test coverageReport coverageAggregate codacyCoverage; fi'
after_success:
- - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash <(curl -s https://codecov.io/bash); fi'
+# - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash <(curl -s https://codecov.io/bash); fi'
diff --git a/README.md b/README.md
index bb0503b..6d161c2 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,14 @@
-# Delphi Command-Line Interface (CLI)
+# Delphi Command-Line Interface (CLI)
The command-line interface for the Delphi platform.
We are currently in pre-alpha state! There is no release and the code in
this repository is purely experimental!
-|branch | status | codacy | snyk |
-| :---: | :---: | :---: | :---: |
-| master | [](https://travis-ci.org/delphi-hub/delphi-cli) | [](https://www.codacy.com/app/delphi-hub/delphi-cli?utm_source=github.com&utm_medium=referral&utm_content=delphi-hub/delphi-cli&utm_campaign=Badge_Grade)| [](https://snyk.io/test/github/delphi-hub/delphi-cli?targetFile=build.sbt) |
-| develop | [](https://travis-ci.org/delphi-hub/delphi-cli) | [](https://www.codacy.com/app/delphi-hub/delphi-cli?branch=develop&utm_source=github.com&utm_medium=referral&utm_content=delphi-hub/delphi-cli&utm_campaign=Badge_Grade)| [](https://snyk.io/test/github/delphi-hub/delphi-cli/develop?targetFile=build.sbt)
+|branch | status | codacy | coverage | snyk |
+| :---: | :---: | :---: | :---: | :---: |
+| master | [](https://travis-ci.org/delphi-hub/delphi-cli) | [](https://www.codacy.com/app/delphi-hub/delphi-cli?utm_source=github.com&utm_medium=referral&utm_content=delphi-hub/delphi-cli&utm_campaign=Badge_Grade) | [](https://www.codacy.com/manual/delphi-hub/delphi-cli?utm_source=github.com&utm_medium=referral&utm_content=delphi-hub/delphi-cli&utm_campaign=Badge_Coverage) | [](https://snyk.io/test/github/delphi-hub/delphi-cli?targetFile=build.sbt) |
+| develop | [](https://travis-ci.org/delphi-hub/delphi-cli) | [](https://www.codacy.com/app/delphi-hub/delphi-cli?branch=develop&utm_source=github.com&utm_medium=referral&utm_content=delphi-hub/delphi-cli&utm_campaign=Badge_Grade)| [](https://www.codacy.com/manual/delphi-hub/delphi-cli?utm_source=github.com&utm_medium=referral&utm_content=delphi-hub/delphi-cli&utm_campaign=Badge_Coverage) | [](https://snyk.io/test/github/delphi-hub/delphi-cli/develop?targetFile=build.sbt)
## What is the Delphi Command-Line Interface?
@@ -42,8 +42,8 @@ Our software is available as a binary release on [GitHub](https://github.com/del
```
$ delphi --help
-Delphi Command Line Tool (1.0.0-SNAPSHOT)
-Usage: delphi [test|retrieve|search] [options] ...
+Delphi Command Line Tool (0.9.5-SNAPSHOT)
+Usage: delphi-cli [test|features|retrieve|search] [options] ...
--version Prints the version of the command line tool.
--help Prints this help text.
diff --git a/build.sbt b/build.sbt
index 3838904..40c03fc 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,11 +1,39 @@
-scalaVersion := "2.12.4"
+import com.typesafe.sbt.packager.docker._
+
+ThisBuild / organization := "de.upb.cs.swt.delphi"
+ThisBuild / organizationName := "Delphi Project"
+ThisBuild / organizationHomepage := Some(url("https://delphi.cs.uni-paderborn.de/"))
+
+ThisBuild / scmInfo := Some(
+ ScmInfo(
+ url("https://github.com/delphi-hub/delphi-cli"),
+ "scm:git@github.com:delphi-hub/delphi-cli.git"
+ )
+)
+
+ThisBuild / developers := List(
+ Developer(
+ id = "bhermann",
+ name = "Ben Hermann",
+ email = "ben.hermann@upb.de",
+ url = url("https://www.thewhitespace.de")
+ )
+)
+
+ThisBuild / description := "The command line client for Delphi"
+ThisBuild / licenses := List("Apache 2" -> new URL("http://www.apache.org/licenses/LICENSE-2.0.txt"))
+ThisBuild / homepage := Some(url("https://delphi.cs.uni-paderborn.de/"))
+
+lazy val scala212 = "2.12.10"
+lazy val scala213 = "2.13.1"
+lazy val supportedScalaVersions = List(scala213)
+
+ThisBuild / scalaVersion := scala213
name := "delphi"
-version := "1.0.0-SNAPSHOT"
+version := "0.9.5"
maintainer := "Ben Hermann "
-licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.html"))
-
packageSummary := "Windows Package for the Delphi CLI"
packageDescription := """Windows Package for the Delphi CLI"""
wixProductId := "ce07be71-510d-414a-92d4-dff47631848a"
@@ -13,35 +41,74 @@ wixProductUpgradeId := "4552fb0e-e257-4dbd-9ecb-dba9dbacf424"
scalastyleConfig := baseDirectory.value / "project" / "scalastyle_config.xml"
-val akkaVersion = "2.5.14"
-val akkaHttpVersion = "10.1.5"
+val http4sVersion = "0.21.0-M6"
+
+// Only necessary for SNAPSHOT releases
+resolvers += Resolver.sonatypeRepo("snapshots")
libraryDependencies ++= Seq(
- "com.typesafe.akka" %% "akka-http-core" % akkaHttpVersion,
- "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion,
- "com.typesafe.akka" %% "akka-stream" % akkaVersion
+ "org.http4s" %% "http4s-dsl" % http4sVersion,
+ "org.http4s" %% "http4s-blaze-client" % http4sVersion,
+ "org.http4s" %% "http4s-circe" % http4sVersion
)
-libraryDependencies += "com.github.scopt" %% "scopt" % "3.7.0"
-libraryDependencies += "io.spray" %% "spray-json" % "1.3.3"
+libraryDependencies += "com.github.scopt" %% "scopt" % "3.7.1"
+libraryDependencies += "io.spray" %% "spray-json" % "1.3.5"
libraryDependencies += "de.vandermeer" % "asciitable" % "0.3.2"
-libraryDependencies += "com.lihaoyi" %% "fansi" % "0.2.5"
-libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
+libraryDependencies += "com.lihaoyi" %% "fansi" % "0.2.7"
+libraryDependencies += "au.com.bytecode" % "opencsv" % "2.4"
+libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.0" % "test"
+libraryDependencies += "joda-time" % "joda-time" % "2.10.5"
+
+libraryDependencies += "de.upb.cs.swt.delphi" %% "delphi-core" % "0.9.2"
+libraryDependencies += "de.upb.cs.swt.delphi" %% "delphi-client" % "0.9.2"
+
+libraryDependencies ++= Seq(
+ "com.softwaremill.sttp" %% "core" % "1.7.2",
+ "com.softwaremill.sttp" %% "spray-json" % "1.7.2"
+)
+
debianPackageDependencies := Seq("java8-runtime-headless")
+mainClass in Compile := Some("de.upb.cs.swt.delphi.cli.DelphiCLI")
+discoveredMainClasses in Compile := Seq()
lazy val cli = (project in file(".")).
enablePlugins(JavaAppPackaging).
enablePlugins(DockerPlugin).
+ settings(
+ dockerBaseImage := "openjdk:jre-alpine",
+ dockerAlias := com.typesafe.sbt.packager.docker.DockerAlias(None, Some("delphihub"),"delphi-cli", Some(version.value)),
+ dockerEntrypoint := Seq("/bin/bash"),
+ dockerCommands ++= Seq(
+ Cmd("USER", "root"),
+ Cmd("RUN", "apk", "--no-cache", "add", "bash"),
+ Cmd("RUN", "ln", "-s", "/opt/docker/bin/delphi", "/usr/bin/delphi" ),
+ Cmd("USER", "daemon")
+ )
+ ).
enablePlugins(ScalastylePlugin).
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",
+ crossScalaVersions := supportedScalaVersions
)
scalastyleConfig := baseDirectory.value / "project" / "scalastyle-config.xml"
trapExit := false
+fork := true
+connectInput := true
diff --git a/project/build.properties b/project/build.properties
index 210243d..7609b47 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version = 1.1.1
+sbt.version = 1.2.8
\ No newline at end of file
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 705f861..58462f1 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,10 +1,10 @@
// 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")
-addSbtPlugin("com.codacy" % "sbt-codacy-coverage" % "1.3.12")
+// addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
+// addSbtPlugin("com.codacy" % "sbt-codacy-coverage" % "1.3.14")
// preparation for dependency checking
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.1")
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/Config.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/Config.scala
index b487a03..fcdb9dc 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/Config.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/Config.scala
@@ -23,18 +23,21 @@ 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 = "",
silent: Boolean = false,
list : Boolean = false,
mode: String = "",
query : String = "",
limit : Option[Int] = None,
id : String = "",
+ timeout : Option[Int] = None,
args: List[String] = List(),
opts: List[String] = List()) {
lazy val consoleOutput = new ConsoleOutput(this)
+ lazy val csvOutput = new CsvOutput(this)
}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/ConsoleOutput.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/ConsoleOutput.scala
index 66f97ea..9268bd7 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/ConsoleOutput.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/ConsoleOutput.scala
@@ -17,6 +17,7 @@
package de.upb.cs.swt.delphi.cli
import de.upb.cs.swt.delphi.cli.artifacts.{RetrieveResult, SearchResult}
+import de.upb.cs.swt.delphi.client.FieldDefinition
class ConsoleOutput(config: Config) {
@@ -45,6 +46,7 @@ class ConsoleOutput(config: Config) {
}
}
case retrieveResults : Seq[RetrieveResult] if retrieveResults.head.isInstanceOf[RetrieveResult] => ResultBeautifier.beautifyRetrieveResults(retrieveResults)
+ case featureResults : Seq[FieldDefinition] if featureResults.head.isInstanceOf[FieldDefinition] => ResultBeautifier.beautifyFeatures(featureResults)
case _ => value.toString
}
}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/CsvOutput.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/CsvOutput.scala
new file mode 100644
index 0000000..6b3e30b
--- /dev/null
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/CsvOutput.scala
@@ -0,0 +1,69 @@
+// Copyright (C) 2018 The Delphi Team.
+// See the LICENCE file distributed with this work for additional
+// information regarding copyright ownership.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package de.upb.cs.swt.delphi.cli
+
+import java.io.{BufferedWriter, FileWriter}
+
+import de.upb.cs.swt.delphi.cli.artifacts.Result
+import au.com.bytecode.opencsv.CSVWriter
+
+import scala.collection.JavaConverters._
+
+/**
+ * Export search and retrieve results to .csv file.
+ *
+ * @author Lisa Nguyen Quang Do
+ * @author Ben Hermann
+ *
+ */
+
+class CsvOutput(config: Config) {
+
+ def exportResult(value: Any): Unit = {
+ printToCsv(
+ value match {
+ case results :
+ Seq[Result] if results.headOption.getOrElse(Seq.empty[Array[String]]).isInstanceOf[Result] => resultsToCsv(results)
+ case _ => Seq.empty[Array[String]]
+ }
+ )
+ }
+
+ def printToCsv(table : Seq[Array[String]]): Unit = {
+ val outputFile = new BufferedWriter(new FileWriter(config.csv, /* append = */false))
+ val csvWriter = new CSVWriter(outputFile)
+ csvWriter.writeAll(seqAsJavaList(table))
+ outputFile.close()
+ }
+
+ def resultsToCsv(results : Seq[Result]) : Seq[Array[String]] = {
+ val headOption = results.headOption.getOrElse()
+ if (!headOption.isInstanceOf[Result]) {
+ Seq.empty[Array[String]]
+ } else {
+ val fieldNames = headOption.asInstanceOf[Result].fieldNames()
+ val tableHeader : Array[String] =
+ fieldNames.+:("discovered at").+:("version").+:("groupId").+:("artifactId").+:("source").+:("Id").toArray
+ results.map {
+ e => {
+ Array(e.id, e.metadata.source, e.metadata.artifactId, e.metadata.groupId, e.metadata.version,
+ e.metadata.discovered).++(fieldNames.map(f => e.metricResults(f).toString))
+ }
+ }.+:(tableHeader)
+ }
+ }
+}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/DelphiCLI.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/DelphiCLI.scala
index c7c45c6..8761820 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/DelphiCLI.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/DelphiCLI.scala
@@ -16,80 +16,93 @@
package de.upb.cs.swt.delphi.cli
-import akka.actor.ActorSystem
-import akka.http.scaladsl.Http
-import akka.stream.ActorMaterializer
-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 javaLibPath = getEnvOrElse("JAVA_LIB_PATH", "/usr/lib/jvm/default-java/lib/")
- val cliParser = {
- new scopt.OptionParser[Config]("delphi-cli") {
- head("Delphi Command Line Tool", s"(${BuildInfo.version})")
+ val trustStorePath = getEnvOrElse("JAVA_TRUSTSTORE", "/usr/lib/jvm/default-java/lib/security/cacerts")
- version("version").text("Prints the version of the command line tool.")
+ // This only is allowed to be set for GraalVM compiles...
+ //System.setProperty("java.library.path", javaLibPath)
+ //System.setProperty("javax.net.ssl.trustStore", trustStorePath)
- help("help").text("Prints this help text.")
- override def showUsageOnError = true
+ cliParser.parse(args, Config()) match {
+ case Some(c) =>
- 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")
- checkConfig(c => if (c.server.isEmpty()) failure("Option server is required.") else success)
+ implicit val config: Config = c
+ implicit val backend: SttpBackend[Id, Nothing] = HttpURLConnectionBackend()
- cmd("test").action((_,c) => c.copy(mode = "test"))
+ if (!config.silent && config.mode != "") cliParser.showHeader()
- 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[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")
- )
+ config.mode match {
+ case "test" => TestCommand.execute
+ case "retrieve" => RetrieveCommand.execute
+ case "search" => SearchCommand.execute
+ case "features" => FeaturesCommand.execute
+ case "" => cliParser.showUsage()
+ case x => config.consoleOutput.outputError(s"Unknown command: $x")
+ }
- 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[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)")
- )
+
+ 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")
+ checkConfig(c => if (c.server.isEmpty()) failure("Option server is required.") else success)
- val poolShutdown = Http().shutdownAllConnectionPools()
- Await.result(poolShutdown, Duration.Inf)
+ cmd("test").action((_, c) => c.copy(mode = "test"))
- implicit val ec: ExecutionContext = system.dispatcher
- val terminationFuture = system.terminate()
+ cmd("features").action((_, c) => c.copy(mode = "features"))
+ .text("Retrieve the current list of features.")
- 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
}
-}
+}
\ No newline at end of file
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/ResultBeautifier.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/ResultBeautifier.scala
index 7f6fc0c..20bbe75 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/ResultBeautifier.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/ResultBeautifier.scala
@@ -17,6 +17,7 @@
package de.upb.cs.swt.delphi.cli
import de.upb.cs.swt.delphi.cli.artifacts.{RetrieveResult, SearchResult}
+import de.upb.cs.swt.delphi.client.FieldDefinition
import de.vandermeer.asciitable.{AsciiTable, CWC_LongestLine}
import de.vandermeer.asciithemes.{TA_Grid, TA_GridThemes}
import de.vandermeer.skb.interfaces.transformers.textformat.TextAlignment
@@ -53,13 +54,13 @@ object ResultBeautifier {
at.getRenderer.setCWC(new CWC_LongestLine)
at.setPaddingLeft(1)
at.setPaddingRight(1)
+
at.getContext.setFrameTopMargin(1)
at.getContext.setFrameBottomMargin(1)
at.getContext().setGridTheme(TA_GridThemes.INSIDE)
at.render()
- //CustomAsciiTable.make(table)
}
}
@@ -112,4 +113,36 @@ object ResultBeautifier {
}.fold("")(_ + _)
}
+ def beautifyFeatures(results : Seq[FieldDefinition]) : String = {
+
+ if (results.size == 0) {
+ ""
+ } else {
+ val tableHeader = Seq ("Name", "Description")
+ val tableBody = results.sortBy(f => f.name).map(f => Seq(f.name, f.description))
+ val table = tableBody.+:(tableHeader)
+
+ val at = new AsciiTable()
+
+ at.setTextAlignment(TextAlignment.JUSTIFIED_LEFT)
+ at.getContext().setWidth(80)
+
+ // add header
+ at.addRule()
+ at.addRow(table.head.asJavaCollection)
+ at.addRule()
+
+ // add body
+ table.tail.foreach { row: Iterable[String] => at.addRow(row.asJavaCollection) }
+
+ at.setPaddingLeft(1)
+ at.setPaddingRight(1)
+ at.getContext.setFrameTopMargin(1)
+ at.getContext.setFrameBottomMargin(1)
+ //at.getContext().setGridTheme(TA_GridThemes.)
+ at.addRule()
+ at.render()
+ }
+ }
+
}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/artifacts/SearchResult.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/artifacts/SearchResult.scala
index ee656bd..19d927c 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/artifacts/SearchResult.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/artifacts/SearchResult.scala
@@ -18,21 +18,23 @@ package de.upb.cs.swt.delphi.cli.artifacts
import spray.json.DefaultJsonProtocol
-case class RetrieveResult(val id: String,
- val metadata: ArtifactMetadata,
- val metricResults: Map[String, Int]) {
+trait Result{
+ val id: String
+ val metadata: ArtifactMetadata
+ val metricResults: Map[String, Int]
+
def toMavenIdentifier() : String = s"${metadata.groupId}:${metadata.artifactId}:${metadata.version}"
def fieldNames() : List[String] = metricResults.keys.toList.sorted
}
-case class SearchResult(val id: String,
- val metadata: ArtifactMetadata,
- val metricResults: Map[String, Int]) {
- def toMavenIdentifier() : String = s"${metadata.groupId}:${metadata.artifactId}:${metadata.version}"
+case class SearchResult(id: String,
+ metadata: ArtifactMetadata,
+ metricResults: Map[String, Int]) extends Result
- def fieldNames() : List[String] = metricResults.keys.toList.sorted
-}
+case class RetrieveResult(id: String,
+ metadata: ArtifactMetadata,
+ metricResults: Map[String, Int]) extends Result
case class ArtifactMetadata(val artifactId: String,
val source: String,
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/artifacts/SearchResults.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/artifacts/SearchResults.scala
new file mode 100644
index 0000000..2617d09
--- /dev/null
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/artifacts/SearchResults.scala
@@ -0,0 +1,26 @@
+package de.upb.cs.swt.delphi.cli.artifacts
+
+import org.joda.time.DateTime
+import de.upb.cs.swt.delphi.cli.artifacts.SearchResultJson._
+import org.joda.time.format.{DateTimeFormatter, ISODateTimeFormat}
+import spray.json.{DefaultJsonProtocol, DeserializationException, JsString, JsValue, RootJsonFormat}
+
+case class SearchResults(totalHits : Long, hits : Array[SearchResult], queried : DateTime = DateTime.now())
+
+
+
+object SearchResultsJson extends DefaultJsonProtocol {
+ implicit object DateJsonFormat extends RootJsonFormat[DateTime] {
+
+ private val parserISO: DateTimeFormatter = ISODateTimeFormat.dateTime()
+
+ override def write(obj: DateTime) = JsString(parserISO.print(obj))
+
+ override def read(json: JsValue): DateTime = json match {
+ case JsString(s) => parserISO.parseDateTime(s)
+ case _ => throw new DeserializationException("Error info you want here ...")
+ }
+ }
+
+ implicit val SearchResultsFormat = jsonFormat3(SearchResults)
+}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/Command.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/Command.scala
index 02cce23..335846b 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/Command.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/Command.scala
@@ -16,18 +16,9 @@
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
*/
@@ -35,49 +26,45 @@ 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 _ =>
- }
-
- 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)
- }
+ 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)
}
-
- 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 _
+
}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/FeaturesCommand.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/FeaturesCommand.scala
new file mode 100644
index 0000000..93e5d6c
--- /dev/null
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/FeaturesCommand.scala
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 The Delphi Team.
+// See the LICENCE file distributed with this work for additional
+// information regarding copyright ownership.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package de.upb.cs.swt.delphi.cli.commands
+import com.softwaremill.sttp.{Id, SttpBackend}
+import de.upb.cs.swt.delphi.cli.Config
+import de.upb.cs.swt.delphi.client.FieldDefinition
+import de.upb.cs.swt.delphi.client.FieldDefinitionJson._
+import spray.json._
+
+object FeaturesCommand extends Command {
+ override def execute(implicit config: Config, backend: SttpBackend[Id, Nothing]): Unit = {
+ val result = executeGet(Seq("features"))
+
+ result.map(features => {
+ if (config.raw) {
+ reportResult.apply(features)
+ }
+ if (!config.raw || !config.csv.equals("")) {
+ val featureList = features.parseJson.convertTo[JsArray].elements.map(e => e.convertTo[FieldDefinition])
+ reportResult.apply(featureList)
+
+ if (!config.csv.equals("")) {
+ exportResult.apply(featureList)
+ information.apply("Results written to file '" + config.csv + "'")
+ }
+ }
+ })
+ }
+}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/RetrieveCommand.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/RetrieveCommand.scala
index 7f4865c..7ceace6 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/RetrieveCommand.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/RetrieveCommand.scala
@@ -16,34 +16,24 @@
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 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
- def checkTarget: String = {
+ val queriedId: String = {
if (config.opts.contains("file")) {
val source = Source.fromFile(config.args.head)
val target = source.getLines.next()
@@ -55,28 +45,21 @@ object RetrieveCommand extends Command with SprayJsonSupport with DefaultJsonPro
}
val result = executeGet(
- s"/retrieve/$checkTarget",
- Map("pretty" -> "")
- )(config, system)
+ Seq("retrieve", queriedId)
+ )
- result.map(s => {
+ result.foreach(s => {
if (config.raw) {
- reportResult(config)(s)
- } else {
- 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)
+ reportResult.apply(s)
+ }
+ if (!config.raw || !config.csv.equals("")) {
+ 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)
- }
+ reportResult.apply(retrieveResults)
+ if (!config.csv.equals("")) {
+ exportResult.apply(retrieveResults)
+ information.apply("Results written to file '" + config.csv + "'")
}
}
})
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/SearchCommand.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/SearchCommand.scala
index f804b8c..e423d3f 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/SearchCommand.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/SearchCommand.scala
@@ -18,99 +18,96 @@ package de.upb.cs.swt.delphi.cli.commands
import java.util.concurrent.TimeUnit
-import akka.actor.ActorSystem
-import akka.http.scaladsl.Http
-import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
-import akka.http.scaladsl.marshalling.Marshal
-import akka.http.scaladsl.model._
-import akka.http.scaladsl.unmarshalling.Unmarshal
-import akka.stream.ActorMaterializer
-import akka.util.ByteString
+import com.softwaremill.sttp._
+import com.softwaremill.sttp.sprayJson._
import de.upb.cs.swt.delphi.cli.Config
-import de.upb.cs.swt.delphi.cli.artifacts.SearchResult
-import de.upb.cs.swt.delphi.cli.artifacts.SearchResultJson._
-import spray.json.DefaultJsonProtocol
+import de.upb.cs.swt.delphi.cli.artifacts.{SearchResult, SearchResults}
+import de.upb.cs.swt.delphi.cli.artifacts.SearchResultsJson._
+import spray.json._
import scala.concurrent.duration._
-import scala.concurrent.{Await, Future}
-import scala.util.{Failure, Success}
-object SearchCommand extends Command with SprayJsonSupport with DefaultJsonProtocol {
+object SearchCommand extends Command with DefaultJsonProtocol{
+
+ val searchTimeout = 10.seconds
+ val timeoutCode = 408
+
/**
* Executes the command implementation
*
* @param config The current configuration for the 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 = {
def query = config.query
- information(config)(s"Searching for artifacts matching ${'"'}$query${'"'}.")
- val start = System.nanoTime()
+ information.apply(s"Searching for artifacts matching ${'"'}$query${'"'}.")
- implicit val queryFormat = jsonFormat2(Query)
- val baseUri = Uri(config.server)
- val prettyParam = Map("pretty" -> "")
- val searchUri = baseUri.withPath(baseUri.path + "/search").withQuery(akka.http.scaladsl.model.Uri.Query(prettyParam))
- val responseFuture = Marshal(Query(query, config.limit)).to[RequestEntity] flatMap { entity =>
- Http().singleRequest(HttpRequest(uri = searchUri, method = HttpMethods.POST, entity = entity))
- }
+ val queryPayload: Query = Query(query,config.limit)
+ val searchUri = uri"${config.server}/search"
- val response = Await.result(responseFuture, 10 seconds)
- val resultFuture: Future[String] = response match {
- case HttpResponse(StatusCodes.OK, headers, entity, _) =>
- entity.dataBytes.runFold(ByteString(""))(_ ++ _).map { body =>
- body.utf8String
- }
- case resp@HttpResponse(code, _, _, _) => {
- error(config)("Request failed, response code: " + code)
- resp.discardEntityBytes()
- Future("")
- }
- }
+ val request = sttp.body(queryPayload.toJson).post(searchUri)
- val result = Await.result(resultFuture, Duration.Inf)
+ val (res, time) = processRequest(request)
+ res.foreach(processResults(_, time))
+ }
- val took = (System.nanoTime() - start).nanos.toUnit(TimeUnit.SECONDS)
+ private def processRequest(req: Request[String, Nothing])
+ (implicit config: Config,
+ backend: SttpBackend[Id, Nothing]): (Option[String], FiniteDuration) = {
+ val start = System.nanoTime()
+ val res: Id[Response[String]] = req.readTimeout(searchTimeout).send()
+ val end = System.nanoTime()
+ val took = (end - start).nanos
- if (config.raw || result.equals("")) {
- reportResult(config)(result)
- } else {
- val unmarshalledFuture = Unmarshal(result).to[List[SearchResult]]
+ if (res.code == timeoutCode) {
- val processFuture = unmarshalledFuture.transform {
- case Success(unmarshalled) => {
- processResults(config, unmarshalled, took)
- Success(unmarshalled)
- }
- case Failure(e) => {
- error(config)(result)
- Failure(e)
- }
- }
+ error.apply(s"The query timed out after ${took.toSeconds}%.0f seconds. " +
+ "To set a longer timeout, use the --timeout option.")
+ }
+ val resStr = res.body match {
+ case Left(v) =>
+ error.apply(s"Search request failed \n $v")
+ None
+ case Right(v) =>
+ Some(v)
}
+ (resStr, took)
}
- private def processResults(config: Config, results: List[SearchResult], queryRuntime: Double) = {
- val capMessage = {
- config.limit match {
- case Some(limit) if (limit <= results.size)
- => s"Results may be capped by result limit set to $limit."
- case None if (results.size >= 50)
- => "Results may be capped by default limit of 50 returned results. Use --limit to extend the result set."
- case _
- => ""
- }
+ private def processResults(res: String, queryRuntime: FiniteDuration)(implicit config: Config) = {
+
+ if (config.raw || res.equals("")) {
+ reportResult.apply(res)
}
- success(config)(s"Found ${results.size} item(s). $capMessage")
- reportResult(config)(results)
+ if (!(config.raw || res.equals("")) || !config.csv.equals("")) {
+ val retrieveResults = res.parseJson.convertTo[SearchResults]
+ val sr = retrieveResults.hits.toList
+ val capMessage = {
+ if (sr.size < retrieveResults.totalHits ) {
+ config.limit match {
+ case Some(limit) if (limit <= sr.size)
+ => s"Results are capped by results limit set to $limit."
+ case None if (sr.size >= 50)
+ => "Results are capped by default limit of 50 returned results. Use --limit to extend the result set."
+ case _
+ => ""
+ }
+ } else {
+ ""
+ }
+ }
- information(config)(f"Query took $queryRuntime%.2fs.")
- }
+ success.apply(s"Found ${retrieveResults.totalHits} item(s). $capMessage")
+ reportResult.apply(sr)
+
+ information.apply(f"Query roundtrip took ${queryRuntime.toUnit(TimeUnit.MILLISECONDS)}%.0fms.")
- case class Query(query: String,
- limit: Option[Int] = None)
+ if (!config.csv.equals("")) {
+ exportResult.apply(sr)
+ information.apply("Results written to file '" + config.csv + "'")
+ }
+ }
+ }
}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/TestCommand.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/TestCommand.scala
index 2589bea..2f11b3e 100644
--- a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/TestCommand.scala
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/TestCommand.scala
@@ -16,7 +16,7 @@
package de.upb.cs.swt.delphi.cli.commands
-import akka.actor.ActorSystem
+import com.softwaremill.sttp.{Id, SttpBackend}
import de.upb.cs.swt.delphi.cli.Config
/**
@@ -24,10 +24,11 @@ import de.upb.cs.swt.delphi.cli.Config
* Tries to connect to the Delphi server and reports on the results of the version call.
*/
object TestCommand extends Command {
- override def execute(config: Config)(implicit system : ActorSystem): Unit = executeGet(
- "/version"
- )(config, system).map(s => {
- success(config)("Successfully contacted Delphi server. ")
- information(config)("Server version: " + s)
- })
+ override def execute(implicit config: Config, backend: SttpBackend[Id, Nothing]): Unit = {
+ executeGet(Seq("version"))
+ .foreach(s => {
+ success.apply("Successfully contacted Delphi server. ")
+ information.apply("Server version: " + s)
+ })
+ }
}
diff --git a/src/main/scala/de/upb/cs/swt/delphi/cli/commands/package.scala b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/package.scala
new file mode 100644
index 0000000..8975631
--- /dev/null
+++ b/src/main/scala/de/upb/cs/swt/delphi/cli/commands/package.scala
@@ -0,0 +1,28 @@
+// Copyright (C) 2018 The Delphi Team.
+// See the LICENCE file distributed with this work for additional
+// information regarding copyright ownership.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package de.upb.cs.swt.delphi.cli
+
+import spray.json._
+
+package object commands extends DefaultJsonProtocol {
+
+
+ case class Query(query: String,
+ limit: Option[Int] = None)
+
+ implicit val queryFormat = jsonFormat2(Query)
+
+}