Skip to content

Commit c9fe3da

Browse files
authored
Merge pull request #45 from delphi-hub/feature/downloadSearchResults
Added CLI options to download JAR and POM files for search results
2 parents f334e24 + 2ab5759 commit c9fe3da

File tree

7 files changed

+241
-6
lines changed

7 files changed

+241
-6
lines changed

src/main/scala/de/upb/cs/swt/delphi/cli/Config.scala

+16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

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

19+
import de.upb.cs.swt.delphi.cli.OutputMode.OutputMode
20+
1921
/**
2022
* Represents a configuration for the Delphi CLI
2123
*
@@ -27,6 +29,8 @@ case class Config(server: String = sys.env.getOrElse("DELPHI_SERVER", "https://d
2729
verbose: Boolean = false,
2830
raw: Boolean = false,
2931
csv: String = "",
32+
output: String = "",
33+
outputMode: Option[OutputMode] = None,
3034
silent: Boolean = false,
3135
list : Boolean = false,
3236
mode: String = "",
@@ -41,3 +45,15 @@ case class Config(server: String = sys.env.getOrElse("DELPHI_SERVER", "https://d
4145
lazy val csvOutput = new CsvOutput(this)
4246

4347
}
48+
49+
object OutputMode extends Enumeration {
50+
type OutputMode = Value
51+
val JarOnly, PomOnly, All = Value
52+
53+
def fromString(value:String): Option[OutputMode] = value.toLowerCase match {
54+
case "jaronly" => Some(JarOnly)
55+
case "pomonly" => Some(PomOnly)
56+
case "all" => Some(All)
57+
case _ => None
58+
}
59+
}

src/main/scala/de/upb/cs/swt/delphi/cli/DelphiCLI.scala

+26-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

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

19+
import java.nio.file.{Files, Paths}
20+
1921
import com.softwaremill.sttp._
2022
import de.upb.cs.swt.delphi.cli.commands._
2123

@@ -89,7 +91,18 @@ object DelphiCLI {
8991
arg[String]("id").action((x, c) => c.copy(id = x)).text("The ID of the project to retrieve"),
9092
opt[String]("csv").action((x, c) => c.copy(csv = x)).text("Path to the output .csv file (overwrites existing file)"),
9193
opt[Unit]('f', "file").action((_, c) => c.copy(opts = List("file"))).text("Use to load the ID from file, " +
92-
"with the filepath given in place of the ID")
94+
"with the filepath given in place of the ID"),
95+
opt[String](name = "output")
96+
.validate(x => if (Files.isDirectory(Paths.get(x))) success else failure(f"Output directory not found at $x"))
97+
.action((x, c) => c.copy(output = x))
98+
.text("Directory to write the result to"),
99+
opt[String](name = "outputmode")
100+
.validate(x => OutputMode.fromString(x) match {
101+
case Some(_) => success
102+
case None => failure("Only JarOnly, PomOnly and All are supported for output modes.")
103+
})
104+
.action((x, c) => c.copy(outputMode = OutputMode.fromString(x)))
105+
.text("Defines what to store. Supported are JarOnly, PomOnly and All. Defaults to PomOnly. Requires output to be set.")
93106
)
94107

95108
cmd("search").action((s, c) => c.copy(mode = "search"))
@@ -99,7 +112,18 @@ object DelphiCLI {
99112
opt[String]("csv").action((x, c) => c.copy(csv = x)).text("Path to the output .csv file (overwrites existing file)"),
100113
opt[Int]("limit").action((x, c) => c.copy(limit = Some(x))).text("The maximal number of results returned."),
101114
opt[Unit](name = "list").action((_, c) => c.copy(list = true)).text("Output results as list (raw option overrides this)"),
102-
opt[Int]("timeout").action((x, c) => c.copy(timeout = Some(x))).text("Timeout in seconds.")
115+
opt[Int]("timeout").action((x, c) => c.copy(timeout = Some(x))).text("Timeout in seconds."),
116+
opt[String](name = "output")
117+
.validate(x => if (Files.isDirectory(Paths.get(x))) success else failure(f"Output directory not found at $x"))
118+
.action((x, c) => c.copy(output = x))
119+
.text("Directory to write the search results to"),
120+
opt[String](name = "outputmode")
121+
.validate(x => OutputMode.fromString(x) match {
122+
case Some(_) => success
123+
case None => failure("Only JarOnly, PomOnly and All are supported for output modes.")
124+
})
125+
.action((x, c) => c.copy(outputMode = OutputMode.fromString(x)))
126+
.text("Defines what to store. Supported are JarOnly, PomOnly and All. Defaults to PomOnly. Requires output to be set.")
103127
)
104128
}
105129
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (C) 2018 The Delphi Team.
2+
// See the LICENCE file distributed with this work for additional
3+
// information regarding copyright ownership.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
package de.upb.cs.swt.delphi.cli
17+
18+
import java.io.{BufferedWriter, FileOutputStream, FileWriter}
19+
import java.nio.file.{Files, Paths}
20+
21+
import com.softwaremill.sttp._
22+
import de.upb.cs.swt.delphi.cli.artifacts.{QueryStorageMetadata, Result, RetrieveResult, SearchResult}
23+
import org.joda.time.DateTime
24+
import org.joda.time.format.DateTimeFormat
25+
import spray.json._
26+
import de.upb.cs.swt.delphi.cli.artifacts.StorageMetadataJson.queryStorageMetadataFormat
27+
28+
class FileOutput (serverVersion: String = "UNKNOWN")(implicit config:Config, backend: SttpBackend[Id, Nothing]){
29+
30+
def writeQueryResults(results: List[SearchResult]): Unit = {
31+
val metadata = buildQueryMetadata(results)
32+
33+
val folderPath = Paths.get(config.output, DateTimeFormat.forPattern("YYYY-MM-dd_HH_mm_ss").print(metadata.timestamp))
34+
Files.createDirectory(folderPath)
35+
36+
downloadResultFiles(results, metadata, folderPath.toString)
37+
}
38+
39+
def writeRetrieveResults(results: Seq[RetrieveResult]): Unit = {
40+
val metadata = buildRetrieveMetadata(results)
41+
val first = results.head
42+
43+
val timestamp = DateTimeFormat.forPattern("YYYY-MM-dd_HH_mm_ss").print(metadata.timestamp)
44+
val folderPath = Paths.get(config.output, s"${first.metadata.artifactId}-${first.metadata.version}-$timestamp")
45+
Files.createDirectory(folderPath)
46+
47+
downloadResultFiles(results, metadata, folderPath.toString)
48+
}
49+
50+
51+
private def downloadResultFiles(results: Seq[Result], metadata: QueryStorageMetadata, folderPath: String) : Unit = {
52+
// Write Metadata first
53+
val metadataPath = Paths.get(folderPath, "query-metadata.json").toString
54+
val writer = new BufferedWriter(new FileWriter(metadataPath))
55+
writer.write(metadata.toJson.prettyPrint)
56+
writer.close()
57+
58+
val outputMode = config.outputMode.getOrElse(OutputMode.PomOnly)
59+
60+
info()
61+
outputMode match {
62+
case OutputMode.PomOnly => info(f"All associated POM files will be stored in $folderPath")
63+
case OutputMode.JarOnly => info(f"All associated JAR files will be stored in $folderPath")
64+
case _ => info(f"All associated JAR and POM files will be stored in $folderPath")
65+
}
66+
var progressCnt = 0f
67+
68+
info()
69+
print("Downloading files: 00 %")
70+
71+
results
72+
.map(r => r.toMavenRelativeUrl() + s"/${r.metadata.artifactId}-${r.metadata.version}")
73+
.map(relUrl => "https://repo1.maven.org/maven2/" + relUrl).foreach( urlWithoutExtension => {
74+
75+
writeProgressValue((100f * progressCnt ).toInt / results.size)
76+
progressCnt += 1
77+
78+
var artifactsToRetrieve = Seq[String]()
79+
if (outputMode == OutputMode.PomOnly || outputMode == OutputMode.All){
80+
artifactsToRetrieve = artifactsToRetrieve ++ Seq(s"$urlWithoutExtension.pom")
81+
}
82+
if(outputMode == OutputMode.JarOnly || outputMode == OutputMode.All){
83+
artifactsToRetrieve = artifactsToRetrieve ++ Seq(s"$urlWithoutExtension.jar")
84+
}
85+
artifactsToRetrieve.foreach( url => {
86+
sttp.get(uri"$url").response(asByteArray).send().body match {
87+
case Right(value) => new FileOutputStream(Paths.get(folderPath, url.splitAt(url.lastIndexOf('/'))._2).toString)
88+
.write(value)
89+
case Left(value) => error(f"Failed to download artifact from $url, got: $value")
90+
}
91+
})
92+
})
93+
writeProgressValue(100)
94+
info()
95+
info()
96+
info(f"Successfully wrote results to $folderPath.")
97+
}
98+
99+
private def info(value: String = ""):Unit = config.consoleOutput.outputInformation(value)
100+
private def error(value: String = ""):Unit = config.consoleOutput.outputError(value)
101+
102+
private def writeProgressValue(progressValue: Int): Unit = {
103+
print("\b\b\b\b")
104+
print(s"${if (progressValue < 10) f"0$progressValue" else progressValue} %")
105+
}
106+
private def buildQueryMetadata(results: List[SearchResult]) =
107+
QueryStorageMetadata( query = config.query,
108+
results = results,
109+
serverVersion = serverVersion,
110+
serverUrl = config.server,
111+
clientVersion = BuildInfo.version,
112+
timestamp = DateTime.now(),
113+
resultLimit = config.limit.getOrElse(50),
114+
outputMode = config.outputMode.getOrElse(OutputMode.PomOnly).toString
115+
)
116+
117+
private def buildRetrieveMetadata(results: Seq[RetrieveResult]) =
118+
QueryStorageMetadata(query = f"Retrieve ${config.id}",
119+
results = results,
120+
serverVersion = serverVersion,
121+
serverUrl = config.server,
122+
clientVersion = BuildInfo.version,
123+
timestamp = DateTime.now(),
124+
resultLimit = 1,
125+
outputMode = config.outputMode.getOrElse(OutputMode.PomOnly).toString
126+
)
127+
}

src/main/scala/de/upb/cs/swt/delphi/cli/artifacts/SearchResult.scala

+13-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package de.upb.cs.swt.delphi.cli.artifacts
1818

19-
import spray.json.DefaultJsonProtocol
19+
import spray.json.{DefaultJsonProtocol, JsValue, RootJsonFormat}
2020

2121
trait Result{
2222
val id: String
@@ -25,6 +25,9 @@ trait Result{
2525

2626
def toMavenIdentifier() : String = s"${metadata.groupId}:${metadata.artifactId}:${metadata.version}"
2727

28+
def toMavenRelativeUrl(): String =
29+
s"${metadata.groupId.replace(".", "/")}/${metadata.artifactId}/${metadata.version}"
30+
2831
def fieldNames() : List[String] = metricResults.keys.toList.sorted
2932
}
3033

@@ -46,4 +49,13 @@ object SearchResultJson extends DefaultJsonProtocol {
4649
implicit val artifactFormat = jsonFormat5(ArtifactMetadata)
4750
implicit val searchResultFormat = jsonFormat3(SearchResult)
4851
implicit val retrieveResultFormat = jsonFormat3(RetrieveResult)
52+
53+
implicit object ResultJsonObject extends RootJsonFormat[Result] {
54+
override def read(json: JsValue): Result = searchResultFormat.read(json)
55+
56+
override def write(obj: Result): JsValue = obj match {
57+
case x: SearchResult => searchResultFormat.write(x)
58+
case x: RetrieveResult => retrieveResultFormat.write(x)
59+
}
60+
}
4961
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (C) 2018 The Delphi Team.
2+
// See the LICENCE file distributed with this work for additional
3+
// information regarding copyright ownership.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package de.upb.cs.swt.delphi.cli.artifacts
18+
19+
import org.joda.time.DateTime
20+
import spray.json.{DefaultJsonProtocol, JsonFormat}
21+
import de.upb.cs.swt.delphi.cli.artifacts.SearchResultJson.ResultJsonObject
22+
import de.upb.cs.swt.delphi.cli.artifacts.SearchResultsJson.DateJsonFormat
23+
24+
trait StorageMetadata {
25+
val clientVersion: String
26+
val serverVersion: String
27+
val serverUrl: String
28+
val timestamp: DateTime
29+
val outputMode: String
30+
}
31+
32+
case class QueryStorageMetadata(query: String,
33+
results: Seq[Result],
34+
resultLimit: Int,
35+
clientVersion: String,
36+
serverVersion: String,
37+
serverUrl: String,
38+
outputMode: String,
39+
timestamp: DateTime) extends StorageMetadata
40+
41+
object StorageMetadataJson extends DefaultJsonProtocol {
42+
implicit val queryStorageMetadataFormat: JsonFormat[QueryStorageMetadata] = jsonFormat8(QueryStorageMetadata)
43+
}

src/main/scala/de/upb/cs/swt/delphi/cli/commands/RetrieveCommand.scala

+7
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ object RetrieveCommand extends Command {
6262
information.apply("Results written to file '" + config.csv + "'")
6363
}
6464
}
65+
if(!config.output.equals("")){
66+
val jsonArr = s.parseJson.asInstanceOf[JsArray].elements
67+
val retrieveResults = jsonArr.map(r => r.convertTo[RetrieveResult])
68+
69+
new FileOutput(executeGet(Seq("version")).getOrElse("UNKNOWN"))
70+
.writeRetrieveResults(retrieveResults)
71+
}
6572
})
6673
}
6774
}

src/main/scala/de/upb/cs/swt/delphi/cli/commands/SearchCommand.scala

+9-3
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import java.util.concurrent.TimeUnit
2020

2121
import com.softwaremill.sttp._
2222
import com.softwaremill.sttp.sprayJson._
23-
import de.upb.cs.swt.delphi.cli.Config
24-
import de.upb.cs.swt.delphi.cli.artifacts.{SearchResult, SearchResults}
23+
import de.upb.cs.swt.delphi.cli.{Config, FileOutput}
24+
import de.upb.cs.swt.delphi.cli.artifacts.SearchResults
2525
import de.upb.cs.swt.delphi.cli.artifacts.SearchResultsJson._
2626
import spray.json._
2727

@@ -75,7 +75,8 @@ object SearchCommand extends Command with DefaultJsonProtocol{
7575
(resStr, took)
7676
}
7777

78-
private def processResults(res: String, queryRuntime: FiniteDuration)(implicit config: Config) = {
78+
private def processResults(res: String, queryRuntime: FiniteDuration)
79+
(implicit config: Config, backend: SttpBackend[Id, Nothing]) = {
7980

8081
if (config.raw || res.equals("")) {
8182
reportResult.apply(res)
@@ -103,6 +104,11 @@ object SearchCommand extends Command with DefaultJsonProtocol{
103104

104105
information.apply(f"Query roundtrip took ${queryRuntime.toUnit(TimeUnit.MILLISECONDS)}%.0fms.")
105106

107+
if (!config.output.equals("")){
108+
val output = new FileOutput(executeGet(Seq("version")).getOrElse("UNKNOWN"))
109+
output.writeQueryResults(sr)
110+
}
111+
106112
if (!config.csv.equals("")) {
107113
exportResult.apply(sr)
108114
information.apply("Results written to file '" + config.csv + "'")

0 commit comments

Comments
 (0)