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 | [![Build Status](https://travis-ci.org/delphi-hub/delphi-cli.svg?branch=master)](https://travis-ci.org/delphi-hub/delphi-cli) | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/47046de0e8d64ae4b76191b7dae80075)](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)| [![Known Vulnerabilities](https://snyk.io/test/github/delphi-hub/delphi-cli/badge.svg?targetFile=build.sbt)](https://snyk.io/test/github/delphi-hub/delphi-cli?targetFile=build.sbt) | -| develop | [![Build Status](https://travis-ci.org/delphi-hub/delphi-cli.svg?branch=develop)](https://travis-ci.org/delphi-hub/delphi-cli) | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/47046de0e8d64ae4b76191b7dae80075?branch=develop)](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)| [![Known Vulnerabilities](https://snyk.io/test/github/delphi-hub/delphi-cli/develop/badge.svg?targetFile=build.sbt)](https://snyk.io/test/github/delphi-hub/delphi-cli/develop?targetFile=build.sbt) +|branch | status | codacy | coverage | snyk | +| :---: | :---: | :---: | :---: | :---: | +| master | [![Build Status](https://travis-ci.org/delphi-hub/delphi-cli.svg?branch=master)](https://travis-ci.org/delphi-hub/delphi-cli) | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/47046de0e8d64ae4b76191b7dae80075)](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) | [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/47046de0e8d64ae4b76191b7dae80075)](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) | [![Known Vulnerabilities](https://snyk.io/test/github/delphi-hub/delphi-cli/badge.svg?targetFile=build.sbt)](https://snyk.io/test/github/delphi-hub/delphi-cli?targetFile=build.sbt) | +| develop | [![Build Status](https://travis-ci.org/delphi-hub/delphi-cli.svg?branch=develop)](https://travis-ci.org/delphi-hub/delphi-cli) | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/47046de0e8d64ae4b76191b7dae80075?branch=develop)](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)| [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/47046de0e8d64ae4b76191b7dae80075)](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) | [![Known Vulnerabilities](https://snyk.io/test/github/delphi-hub/delphi-cli/develop/badge.svg?targetFile=build.sbt)](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) + +}