diff --git a/app.yml b/app.yml
index f2f9a3a..ff4e30d 100644
--- a/app.yml
+++ b/app.yml
@@ -1,6 +1,7 @@
dependencies:
eu.maveniverse.maven.mima:context: "2.4.33"
eu.maveniverse.maven.mima.runtime:standalone-static: "2.4.33"
+ org.apache.maven.indexer:search-backend-smo: "7.1.6"
info.picocli:picocli: "4.7.7"
org.yaml:snakeyaml: "2.4"
org.jline:jline-console-ui: "3.30.5"
diff --git a/pom.xml b/pom.xml
index 51a9d2a..48b02e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,6 +33,7 @@
11
org.codejive.jpm.Main
2.4.34
+ 7.1.6
4.7.7
2.4
3.30.5
@@ -55,6 +56,11 @@
standalone-static
${version.mima}
+
+ org.apache.maven.indexer
+ search-backend-smo
+ ${version.search-backend}
+
info.picocli
picocli
@@ -126,11 +132,21 @@
maven-compiler-plugin
3.14.0
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.6.1
+
org.apache.maven.plugins
maven-surefire-plugin
3.5.3
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.5.3
+
org.apache.maven.plugins
maven-jar-plugin
@@ -210,6 +226,38 @@
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-test-source
+ process-resources
+
+ add-test-source
+
+
+
+ src/it/java
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ integration-test
+
+ integration-test
+ verify
+
+ integration-test
+
+
+
org.apache.maven.plugins
maven-compiler-plugin
diff --git a/src/it/java/org/codejive/jpm/MainIT.java b/src/it/java/org/codejive/jpm/MainIT.java
new file mode 100644
index 0000000..e370d58
--- /dev/null
+++ b/src/it/java/org/codejive/jpm/MainIT.java
@@ -0,0 +1,161 @@
+package org.codejive.jpm;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
+
+/** Integration tests for the Main class, focusing on the new 'do' command and aliases. */
+class MainIT {
+
+ @TempDir Path tempDir;
+
+ private String originalDir;
+
+ @BeforeEach
+ void setUp() {
+ originalDir = System.getProperty("user.dir");
+ System.setProperty("user.dir", tempDir.toString());
+ System.setProperty("picocli.ansi", "false");
+ }
+
+ @AfterEach
+ void tearDown() {
+ System.setProperty("user.dir", originalDir);
+ }
+
+ /**
+ * Helper method to capture stdout/stderr for tests that need to check command output. Only use
+ * this for tests that check jpm's own output, not for tests that execute system commands.
+ */
+ private TestOutputCapture captureOutput() {
+ PrintStream originalOut = System.out;
+ PrintStream originalErr = System.err;
+ ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ ByteArrayOutputStream errContent = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outContent));
+ System.setErr(new PrintStream(errContent));
+
+ return new TestOutputCapture(originalOut, originalErr, outContent, errContent);
+ }
+
+ /** Helper class to manage output capture and restoration */
+ private static class TestOutputCapture implements AutoCloseable {
+ private final PrintStream originalOut;
+ private final PrintStream originalErr;
+ private final ByteArrayOutputStream outContent;
+ private final ByteArrayOutputStream errContent;
+
+ TestOutputCapture(
+ PrintStream originalOut,
+ PrintStream originalErr,
+ ByteArrayOutputStream outContent,
+ ByteArrayOutputStream errContent) {
+ this.originalOut = originalOut;
+ this.originalErr = originalErr;
+ this.outContent = outContent;
+ this.errContent = errContent;
+ }
+
+ String getOut() {
+ return outContent.toString();
+ }
+
+ String getErr() {
+ return errContent.toString();
+ }
+
+ @Override
+ public void close() {
+ System.setOut(originalOut);
+ System.setErr(originalErr);
+ }
+ }
+
+ @Test
+ void testDoAliasWithArgs() throws IOException {
+ createAppYmlWithRepositories();
+ try (TestOutputCapture capture = captureOutput()) {
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode = cmd.execute("build", "--foo", "bar");
+
+ assertThat(exitCode).isEqualTo(0);
+ String output = capture.getOut();
+ // The run action should execute and include the classpath in the output
+ assertThat(output).contains("javac", "jfiglet", "--foo bar");
+ }
+ }
+
+ @Test
+ void testCopyCommandWithRepositoryOptions() throws IOException {
+ // Test copy command with --repo options
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode =
+ cmd.execute(
+ "copy",
+ "--repo",
+ "central=https://repo1.maven.org/maven2",
+ "--repo",
+ "https://jcenter.bintray.com",
+ "com.google.guava:guava:31.1-jre");
+
+ // The command should execute successfully (even if dependency resolution might fail)
+ assertThat(exitCode >= 0).isTrue();
+ }
+
+ @Test
+ void testInstallCommandWithRepositoryOptions() throws IOException {
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode =
+ cmd.execute(
+ "install",
+ "--repo",
+ "central=https://repo1.maven.org/maven2",
+ "com.google.guava:guava:31.1-jre");
+
+ // The command should execute successfully (even if dependency resolution might fail)
+ assertThat(exitCode >= 0).isTrue();
+ }
+
+ @Test
+ void testPathCommandWithRepositoryOptionsAndAppYml() throws IOException {
+ // Create app.yml with repositories
+ createAppYmlWithRepositories();
+
+ try (TestOutputCapture capture = captureOutput()) {
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode =
+ cmd.execute(
+ "path",
+ "--repo",
+ "jcenter=https://jcenter.bintray.com",
+ "com.google.guava:guava:31.1-jre");
+
+ // The command should execute (even if dependency resolution might fail)
+ assertThat(exitCode >= 0).isTrue();
+ }
+ }
+
+ private void createAppYmlWithRepositories() throws IOException {
+ String yamlContent =
+ "dependencies:\n"
+ + " com.github.lalyos:jfiglet: \"0.0.9\"\n"
+ + "\n"
+ + "repositories:\n"
+ + " central: \"https://repo1.maven.org/maven2\"\n"
+ + " custom: \"https://my.custom.repo/maven2\"\n"
+ + "\n"
+ + "actions:\n"
+ + " build: \"echo javac -cp {{deps}} *.java\"\n"
+ + " test: \"echo java -cp {{deps}} TestRunner\"\n";
+ Files.writeString(tempDir.resolve("app.yml"), yamlContent);
+ }
+}
diff --git a/src/it/java/org/codejive/jpm/search/SearchIT.java b/src/it/java/org/codejive/jpm/search/SearchIT.java
new file mode 100644
index 0000000..ab17bb4
--- /dev/null
+++ b/src/it/java/org/codejive/jpm/search/SearchIT.java
@@ -0,0 +1,54 @@
+package org.codejive.jpm.search;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.codejive.jpm.search.Search;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.FieldSource;
+
+public class SearchIT {
+ @ParameterizedTest
+ @EnumSource(Search.Backends.class)
+ void testSearchSingleTerm(Search.Backends backend) throws IOException {
+ Search s = Search.getBackend(backend);
+ Search.SearchResult res = s.findArtifacts("httpclient", 10);
+ assertThat(res.count).isGreaterThan(1);
+ assertThat(res.artifacts).isNotEmpty();
+ res = s.findNextArtifacts(res);
+ assertThat(res.count).isGreaterThan(1);
+ assertThat(res.artifacts).isNotEmpty();
+ }
+
+ @ParameterizedTest
+ @EnumSource(Search.Backends.class)
+ void testSearchDoubleTerm(Search.Backends backend) throws IOException {
+ Search s = Search.getBackend(backend);
+ Search.SearchResult res = s.findArtifacts("apache:httpclient", 10);
+ assertThat(res.count).isGreaterThan(1);
+ assertThat(res.artifacts).isNotEmpty();
+ res = s.findNextArtifacts(res);
+ assertThat(res.count).isGreaterThan(1);
+ assertThat(res.artifacts).isNotEmpty();
+ }
+
+ @ParameterizedTest
+ @EnumSource(Search.Backends.class)
+ void testSearchTripleTerm(Search.Backends backend) throws IOException {
+ Search s = Search.getBackend(backend);
+ Search.SearchResult res = s.findArtifacts("org.apache.httpcomponents:httpclient:", 10);
+ assertThat(res.count).isGreaterThan(1);
+ assertThat(res.artifacts).isNotEmpty();
+ assertThat(res.artifacts).allMatch(a -> "org.apache.httpcomponents".equals(a.getGroupId()));
+ assertThat(res.artifacts).allMatch(a -> "httpclient".equals(a.getArtifactId()));
+ res = s.findNextArtifacts(res);
+ assertThat(res.count).isGreaterThan(1);
+ assertThat(res.artifacts).isNotEmpty();
+ assertThat(res.artifacts).allMatch(a -> "org.apache.httpcomponents".equals(a.getGroupId()));
+ assertThat(res.artifacts).allMatch(a -> "httpclient".equals(a.getArtifactId()));
+ }
+}
diff --git a/src/main/java/org/codejive/jpm/Jpm.java b/src/main/java/org/codejive/jpm/Jpm.java
index 8ea5f0e..856c0bc 100644
--- a/src/main/java/org/codejive/jpm/Jpm.java
+++ b/src/main/java/org/codejive/jpm/Jpm.java
@@ -5,6 +5,7 @@
import java.util.*;
import java.util.stream.Collectors;
import org.codejive.jpm.config.AppInfo;
+import org.codejive.jpm.search.Search;
import org.codejive.jpm.util.*;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.resolution.DependencyResolutionException;
@@ -120,12 +121,26 @@ public SyncResult copy(String[] artifactNames, Map repos, boolea
* @throws IOException If an error occurred during the search.
*/
public String[] search(String artifactPattern, int count) throws IOException {
+ return search(artifactPattern, count, null);
+ }
+
+ /**
+ * Searches for artifacts matching the given pattern.
+ *
+ * @param artifactPattern The pattern to search for.
+ * @param count The maximum number of results to return.
+ * @return An array of artifact names matching the given pattern.
+ * @throws IOException If an error occurred during the search.
+ */
+ public String[] search(String artifactPattern, int count, Search.Backends backend)
+ throws IOException {
List artifacts = new ArrayList<>();
int max = count <= 0 || count > 200 ? 200 : count;
- SearchResult result = SearchUtils.findArtifacts(artifactPattern, max);
+ Search s = Search.getBackend(backend);
+ Search.SearchResult result = s.findArtifacts(artifactPattern, max);
while (result != null) {
artifacts.addAll(result.artifacts);
- result = count <= 0 ? SearchUtils.findNextArtifacts(result) : null;
+ result = count <= 0 ? s.findNextArtifacts(result) : null;
}
return artifacts.stream().map(Jpm::artifactGav).toArray(String[]::new);
}
diff --git a/src/main/java/org/codejive/jpm/Main.java b/src/main/java/org/codejive/jpm/Main.java
index e17ec4d..2670097 100644
--- a/src/main/java/org/codejive/jpm/Main.java
+++ b/src/main/java/org/codejive/jpm/Main.java
@@ -1,11 +1,13 @@
// spotless:off Dependencies for JBang
//DEPS eu.maveniverse.maven.mima:context:2.4.34 eu.maveniverse.maven.mima.runtime:standalone-static:2.4.34
+//DEPS org.apache.maven.indexer:search-backend-smo:7.1.6
//DEPS info.picocli:picocli:4.7.7
//DEPS org.yaml:snakeyaml:2.4
//DEPS org.jline:jline-console-ui:3.30.5 org.jline:jline-terminal-jni:3.30.5
//DEPS org.slf4j:slf4j-api:2.0.17 org.slf4j:slf4j-simple:2.0.17
-//SOURCES Jpm.java config/AppInfo.java util/CommandsParser.java util/FileUtils.java util/Resolver.java
-//SOURCES util/ScriptUtils.java util/SearchResult.java util/SearchUtils.java util/SyncResult.java util/Version.java
+//SOURCES Jpm.java config/AppInfo.java search/Search.java search/SearchSmoRestImpl.java search/SearchSmoApiImpl.java
+//SOURCES util/CommandsParser.java util/FileUtils.java util/Resolver.java util/ScriptUtils.java util/SyncResult.java
+//SOURCES util/Version.java
// spotless:on
package org.codejive.jpm;
@@ -17,6 +19,7 @@
import java.util.*;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
+import org.codejive.jpm.search.Search.Backends;
import org.codejive.jpm.util.SyncResult;
import org.codejive.jpm.util.Version;
import org.jline.consoleui.elements.InputValue;
@@ -45,16 +48,16 @@
versionProvider = Version.class,
description = "Simple command line tool for managing Maven artifacts",
subcommands = {
- Main.Copy.class,
Main.Search.class,
Main.Install.class,
+ Main.Copy.class,
Main.PrintPath.class,
- Main.Exec.class,
Main.Do.class,
Main.Clean.class,
Main.Build.class,
Main.Run.class,
- Main.Test.class
+ Main.Test.class,
+ Main.Exec.class
})
public class Main {
@@ -118,6 +121,12 @@ static class Search implements Callable {
description = "Maximum number of results to return")
private Integer max;
+ @Option(
+ names = {"-b", "--backend"},
+ description =
+ "The search backend to use. Supported values: ${COMPLETION-CANDIDATES}")
+ private Backends backend;
+
@Parameters(
paramLabel = "artifactPattern",
description = "Partial or full artifact name to search for.",
@@ -194,7 +203,7 @@ String[] search(String artifactPattern) {
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.build()
- .search(artifactPattern, Math.min(max, 200));
+ .search(artifactPattern, Math.min(max, 200), backend);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
diff --git a/src/main/java/org/codejive/jpm/search/Search.java b/src/main/java/org/codejive/jpm/search/Search.java
new file mode 100644
index 0000000..e9db3cd
--- /dev/null
+++ b/src/main/java/org/codejive/jpm/search/Search.java
@@ -0,0 +1,96 @@
+package org.codejive.jpm.search;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.aether.artifact.Artifact;
+
+public interface Search {
+
+ /**
+ * Find artifacts matching the given pattern. This will return the first page of results. If the
+ * pattern to search for is a simple name (there are no colons in the string), the search will
+ * match any part of an artifact's group or name. If there's a single colon, the search will
+ * match any part of the group id and artifact id separately. If there are two colons, the
+ * search will match the group id and artifact id exactly, and will return the artifact's
+ * versions.
+ *
+ * @param artifactPattern The pattern to search for.
+ * @param count The maximum number of results to return.
+ * @return The search result as an instance of {@link SearchResult}.
+ * @throws IOException If an error occurred during the search.
+ */
+ SearchResult findArtifacts(String artifactPattern, int count) throws IOException;
+
+ /**
+ * Find the next page of artifacts. This takes a {@link SearchResult} returned by a previous
+ * call to {@link #findArtifacts(String, int)} and returns the next page of results.
+ *
+ * @param prevResult The previous search result.
+ * @return The next search result as an instance of {@link SearchResult}.
+ * @throws IOException If an error occurred during the search.
+ */
+ SearchResult findNextArtifacts(SearchResult prevResult) throws IOException;
+
+ enum Backends {
+ rest_smo,
+ rest_csc;
+ // smo_smo,
+ // smo_csc;
+ }
+
+ static Search getBackend(Backends backend) {
+ if (backend != null) {
+ switch (backend) {
+ case rest_smo:
+ return SearchSolrRestImpl.createSmo();
+ case rest_csc:
+ return SearchSolrRestImpl.createCsc();
+ // case smo_smo:
+ // return SearchSmoApiImpl.createSmo();
+ // case smo_csc:
+ // return SearchSmoApiImpl.createCsc();
+ }
+ }
+ return SearchSolrRestImpl.createSmo();
+ }
+
+ /**
+ * Hold the result of a search while also functioning as a kind of bookmark for paging purposes.
+ */
+ class SearchResult {
+ /** The artifacts that matched the search query. */
+ public final List extends Artifact> artifacts;
+
+ /** The search query that produced this result. */
+ public final String query;
+
+ /** The index of the first artifact in this result relative to the total result set. */
+ public final int start;
+
+ /** The maximum number of results to return */
+ public final int count;
+
+ /** The total number of artifacts that matched the search query. */
+ public final int total;
+
+ /**
+ * Create a new search result.
+ *
+ * @param artifacts The artifacts that matched the search query.
+ * @param query The search query that produced this result.
+ * @param start The index of the first artifact in this result relative to the total result
+ * set.
+ * @param count The maximum number of results to return.
+ * @param total The total number of artifacts that matched the search query.
+ */
+ public SearchResult(
+ List extends Artifact> artifacts, String query, int start, int count, int total) {
+ this.artifacts = Collections.unmodifiableList(artifacts);
+ this.query = query;
+ this.start = start;
+ this.count = count;
+ this.total = total;
+ }
+ }
+}
diff --git a/src/main/java/org/codejive/jpm/search/SearchSmoApiImpl.java b/src/main/java/org/codejive/jpm/search/SearchSmoApiImpl.java
new file mode 100644
index 0000000..56dc6a7
--- /dev/null
+++ b/src/main/java/org/codejive/jpm/search/SearchSmoApiImpl.java
@@ -0,0 +1,92 @@
+package org.codejive.jpm.search;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.maven.search.api.MAVEN;
+import org.apache.maven.search.api.Record;
+import org.apache.maven.search.api.SearchRequest;
+import org.apache.maven.search.api.request.BooleanQuery;
+import org.apache.maven.search.api.request.FieldQuery;
+import org.apache.maven.search.api.request.Paging;
+import org.apache.maven.search.api.request.Query;
+import org.apache.maven.search.backend.smo.SmoSearchBackend;
+import org.apache.maven.search.backend.smo.SmoSearchBackendFactory;
+import org.apache.maven.search.backend.smo.SmoSearchResponse;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+public class SearchSmoApiImpl implements Search {
+ private final SmoSearchBackend backend;
+
+ public static Search createSmo() {
+ return new SearchSmoApiImpl(SmoSearchBackendFactory.createSmo());
+ }
+
+ public static Search createCsc() {
+ return new SearchSmoApiImpl(SmoSearchBackendFactory.createCsc());
+ }
+
+ private SearchSmoApiImpl(SmoSearchBackend backend) {
+ this.backend = backend;
+ }
+
+ @Override
+ public SearchResult findArtifacts(String query, int count) throws IOException {
+ return select(query, 0, count);
+ }
+
+ @Override
+ public SearchResult findNextArtifacts(SearchResult prevResult) throws IOException {
+ return select(prevResult.query, prevResult.start + 1, prevResult.count);
+ }
+
+ private SearchResult select(String query, int start, int count) throws IOException {
+ String[] parts = query.split(":", -1);
+ Query q;
+ if (parts.length >= 3) {
+ // Exact group/artifact match for retrieving versions
+ q =
+ BooleanQuery.and(
+ FieldQuery.fieldQuery(MAVEN.GROUP_ID, parts[0]),
+ FieldQuery.fieldQuery(MAVEN.ARTIFACT_ID, parts[1]));
+ } else if (parts.length == 2) {
+ // Partial group/artifact match, we will filter the results
+ // to remove those that match an inverted artifact/group
+ q = Query.query(String.format("%s %s", parts[0], parts[1]));
+ } else {
+ // Simple partial match
+ q = Query.query(query);
+ }
+ SearchRequest req = new SearchRequest(new Paging(count, start), q);
+ SmoSearchResponse res = backend.search(req);
+ List artifacts =
+ res.getPage().stream()
+ .filter(r -> acceptRecord(r, parts))
+ .map(SearchSmoApiImpl::toArtifact)
+ .collect(Collectors.toList());
+ return new SearchResult(
+ artifacts,
+ query,
+ req.getPaging().getPageOffset(),
+ res.getCurrentHits(),
+ res.getTotalHits());
+ }
+
+ private static boolean acceptRecord(Record r, String[] parts) {
+ String grp = r.getValue(MAVEN.GROUP_ID);
+ String art = r.getValue(MAVEN.ARTIFACT_ID);
+ String pkg = r.getValue(MAVEN.PACKAGING);
+ return pkg != null
+ && grp != null
+ && art != null
+ && (pkg.equals("jar") || pkg.equals("bundle"))
+ && (parts.length != 2 || (grp.contains(parts[0]) && art.contains(parts[1])));
+ }
+
+ private static DefaultArtifact toArtifact(Record r) {
+ String grp = r.getValue(MAVEN.GROUP_ID);
+ String art = r.getValue(MAVEN.ARTIFACT_ID);
+ String ver = r.getValue(MAVEN.VERSION);
+ return new DefaultArtifact(grp, art, "", ver);
+ }
+}
diff --git a/src/main/java/org/codejive/jpm/util/SearchUtils.java b/src/main/java/org/codejive/jpm/search/SearchSolrRestImpl.java
similarity index 57%
rename from src/main/java/org/codejive/jpm/util/SearchUtils.java
rename to src/main/java/org/codejive/jpm/search/SearchSolrRestImpl.java
index bd0c1c0..6ae69a5 100644
--- a/src/main/java/org/codejive/jpm/util/SearchUtils.java
+++ b/src/main/java/org/codejive/jpm/search/SearchSolrRestImpl.java
@@ -1,4 +1,4 @@
-package org.codejive.jpm.util;
+package org.codejive.jpm.search;
import java.io.IOException;
import java.io.InputStream;
@@ -10,6 +10,7 @@
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
+import org.codejive.jpm.util.Version;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
@@ -18,7 +19,22 @@
import org.yaml.snakeyaml.representer.Representer;
/** Utility class for searching Maven artifacts. */
-public class SearchUtils {
+public class SearchSolrRestImpl implements Search {
+ private final String baseSearchUrl;
+ private final boolean offsetInPages;
+
+ public static SearchSolrRestImpl createSmo() {
+ return new SearchSolrRestImpl("https://search.maven.org/solrsearch/select", false);
+ }
+
+ public static SearchSolrRestImpl createCsc() {
+ return new SearchSolrRestImpl("https://central.sonatype.com/solrsearch/select", true);
+ }
+
+ private SearchSolrRestImpl(String baseSearchUrl, boolean offsetInPages) {
+ this.baseSearchUrl = baseSearchUrl;
+ this.offsetInPages = offsetInPages;
+ }
/**
* Find artifacts matching the given pattern. This will return the first page of results. If the
@@ -30,31 +46,38 @@ public class SearchUtils {
*
* @param artifactPattern The pattern to search for.
* @param count The maximum number of results to return.
- * @return The search result as an instance of {@link SearchResult}.
+ * @return The search result as an instance of {@link Search.SearchResult}.
* @throws IOException If an error occurred during the search.
*/
- public static SearchResult findArtifacts(String artifactPattern, int count) throws IOException {
+ public Search.SearchResult findArtifacts(String artifactPattern, int count) throws IOException {
return select(artifactPattern, 0, count);
}
/**
- * Find the next page of artifacts. This takes a {@link SearchResult} returned by a previous
- * call to {@link #findArtifacts(String, int)} and returns the next page of results.
+ * Find the next page of artifacts. This takes a {@link Search.SearchResult} returned by a
+ * previous call to {@link #findArtifacts(String, int)} and returns the next page of results.
*
* @param prevResult The previous search result.
- * @return The next search result as an instance of {@link SearchResult}.
+ * @return The next search result as an instance of {@link Search.SearchResult}.
* @throws IOException If an error occurred during the search.
*/
- public static SearchResult findNextArtifacts(SearchResult prevResult) throws IOException {
- if (prevResult.start + prevResult.count >= prevResult.total) {
- return null;
+ public Search.SearchResult findNextArtifacts(Search.SearchResult prevResult)
+ throws IOException {
+ if (offsetInPages) {
+ if ((prevResult.start + 1) * prevResult.count >= prevResult.total) {
+ return null;
+ }
+ } else {
+ if (prevResult.start + prevResult.count >= prevResult.total) {
+ return null;
+ }
}
- SearchResult result =
- select(prevResult.query, prevResult.start + prevResult.count, prevResult.count);
+ int start = offsetInPages ? prevResult.start + 1 : prevResult.start + prevResult.count;
+ Search.SearchResult result = select(prevResult.query, start, prevResult.count);
return result.artifacts.isEmpty() ? null : result;
}
- private static SearchResult select(String query, int start, int count) throws IOException {
+ private Search.SearchResult select(String query, int start, int count) throws IOException {
String[] parts = query.split(":", -1);
String finalQuery;
if (parts.length >= 3) {
@@ -63,15 +86,17 @@ private static SearchResult select(String query, int start, int count) throws IO
} else if (parts.length == 2) {
// Partial group/artifact match, we will filter the results
// to remove those that match an inverted artifact/group
- finalQuery = String.format("%s AND %s", parts[0], parts[1]);
+ finalQuery = String.format("%s %s", parts[0], parts[1]);
} else {
// Simple partial match
finalQuery = query;
}
String searchUrl =
String.format(
- "https://search.maven.org/solrsearch/select?start=%d&rows=%d&q=%s",
- start, count, URLEncoder.encode(finalQuery, "UTF-8"));
+ this.baseSearchUrl + "?start=%d&rows=%d&q=%s",
+ start,
+ count,
+ URLEncoder.encode(finalQuery, "UTF-8"));
if (parts.length >= 3) {
searchUrl += "&core=gav";
}
@@ -84,38 +109,50 @@ private static SearchResult select(String query, int start, int count) throws IO
try (CloseableHttpClient httpClient = HttpClients.custom().setUserAgent(agent).build()) {
HttpGet request = new HttpGet(searchUrl);
try (CloseableHttpResponse response = httpClient.execute(request)) {
- if (response.getStatusLine().getStatusCode() != 200) {
- throw new IOException(
+ int code = response.getStatusLine().getStatusCode();
+ if (code != 200) {
+ String err =
"Search failed: Maven Central Search API returned an error: "
+ response.getStatusLine().getStatusCode()
+ " "
- + response.getStatusLine().getReasonPhrase());
+ + response.getStatusLine().getReasonPhrase();
+ if (code >= 500 && code < 600) {
+ err +=
+ ". The service might be temporarily unavailable. You can try with a different search backend, run again using the -b option. ";
+ }
+ throw new IOException(err);
}
- DumperOptions dopts = new DumperOptions();
- Constructor cons = new Constructor(MvnSearchResult.class, new LoaderOptions());
- Representer representer = new Representer(dopts);
- representer.getPropertyUtils().setSkipMissingProperties(true);
- Yaml yaml = new Yaml(cons, representer, dopts);
InputStream ins = response.getEntity().getContent();
- InputStreamReader rdr = new InputStreamReader(ins);
- MvnSearchResult result = yaml.load(rdr);
- if (result.responseHeader.status != 0) {
- throw new IOException(
- "Search failed: Maven Central Search API returned a response that could not be understood");
- }
+ MvnSearchResult result = parseSearchResult(ins);
List artifacts =
result.response.docs.stream()
.filter(d -> acceptDoc(d, parts))
- .map(SearchUtils::toArtifact)
+ .map(SearchSolrRestImpl::toArtifact)
.collect(Collectors.toList());
- return new SearchResult(artifacts, query, start, count, result.response.numFound);
+ return new Search.SearchResult(
+ artifacts, query, start, count, result.response.numFound);
}
}
}
+ private static MvnSearchResult parseSearchResult(InputStream ins) throws IOException {
+ DumperOptions dopts = new DumperOptions();
+ Constructor cons = new Constructor(MvnSearchResult.class, new LoaderOptions());
+ Representer representer = new Representer(dopts);
+ representer.getPropertyUtils().setSkipMissingProperties(true);
+ Yaml yaml = new Yaml(cons, representer, dopts);
+ InputStreamReader rdr = new InputStreamReader(ins);
+ MvnSearchResult result = yaml.load(rdr);
+ if (result.responseHeader.status != 0) {
+ throw new IOException(
+ "Search failed: Maven Search API did not return a valid response");
+ }
+ return result;
+ }
+
private static boolean acceptDoc(MsrDoc d, String[] parts) {
return d.ec != null
- && d.ec.contains(".jar")
+ && (d.ec.contains(".jar") || d.ec.contains("jar"))
&& (parts.length != 2 || d.g.contains(parts[0]) && d.a.contains(parts[1]));
}
diff --git a/src/main/java/org/codejive/jpm/util/SearchResult.java b/src/main/java/org/codejive/jpm/util/SearchResult.java
deleted file mode 100644
index ab2d601..0000000
--- a/src/main/java/org/codejive/jpm/util/SearchResult.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.codejive.jpm.util;
-
-import java.util.Collections;
-import java.util.List;
-import org.eclipse.aether.artifact.Artifact;
-
-/** Hold the result of a search while also functioning as a kind of bookmark for paging purposes. */
-public class SearchResult {
- /** The artifacts that matched the search query. */
- public final List extends Artifact> artifacts;
-
- /** The search query that produced this result. */
- public final String query;
-
- /** The index of the first artifact in this result relative to the total result set. */
- public final int start;
-
- /** The maximum number of results to return */
- public final int count;
-
- /** The total number of artifacts that matched the search query. */
- public final int total;
-
- /**
- * Create a new search result.
- *
- * @param artifacts The artifacts that matched the search query.
- * @param query The search query that produced this result.
- * @param start The index of the first artifact in this result relative to the total result set.
- * @param count The maximum number of results to return.
- * @param total The total number of artifacts that matched the search query.
- */
- public SearchResult(
- List extends Artifact> artifacts, String query, int start, int count, int total) {
- this.artifacts = Collections.unmodifiableList(artifacts);
- this.query = query;
- this.start = start;
- this.count = count;
- this.total = total;
- }
-}
diff --git a/src/test/java/org/codejive/jpm/MainIntegrationTest.java b/src/test/java/org/codejive/jpm/MainTest.java
similarity index 73%
rename from src/test/java/org/codejive/jpm/MainIntegrationTest.java
rename to src/test/java/org/codejive/jpm/MainTest.java
index 85a11c4..740d546 100644
--- a/src/test/java/org/codejive/jpm/MainIntegrationTest.java
+++ b/src/test/java/org/codejive/jpm/MainTest.java
@@ -1,6 +1,6 @@
package org.codejive.jpm;
-import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThat;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -14,7 +14,7 @@
import picocli.CommandLine;
/** Integration tests for the Main class, focusing on the new 'do' command and aliases. */
-class MainIntegrationTest {
+class MainTest {
@TempDir Path tempDir;
@@ -24,6 +24,7 @@ class MainIntegrationTest {
void setUp() {
originalDir = System.getProperty("user.dir");
System.setProperty("user.dir", tempDir.toString());
+ System.setProperty("picocli.ansi", "false");
}
@AfterEach
@@ -235,14 +236,16 @@ void testDoWithOutput() throws IOException {
@Test
void testMainWithNoArgs() {
- // Test the default behavior using CommandLine
- CommandLine cmd = Main.getCommandLine();
- int exitCode = cmd.execute();
-
- // Should show help when no args provided (CommandLine default behavior)
- // The Main.main() method redirects to interactive search, but CommandLine.execute()
- // with no args typically shows help
- assertThat(exitCode >= 0).isTrue(); // Should not be negative (internal error)
+ try (TestOutputCapture capture = captureOutput()) {
+ // Test the default behavior using CommandLine
+ CommandLine cmd = Main.getCommandLine();
+ int exitCode = cmd.execute();
+ assertThat(exitCode >= 0).isTrue(); // Should not be negative (internal error)
+ assertThat(capture.getErr()).contains("Missing required subcommand");
+ assertThat(capture.getErr()).contains("Usage: jpm [-hV] [COMMAND]");
+ assertThat(capture.getErr())
+ .contains("Simple command line tool for managing Maven artifacts");
+ }
}
@Test
@@ -259,97 +262,32 @@ void testDoAliasWithArgs() throws IOException {
}
}
- @Test
- void testCopyCommandWithRepositoryOptions() throws IOException {
- // Test copy command with --repo options
- CommandLine cmd = Main.getCommandLine();
- int exitCode =
- cmd.execute(
- "copy",
- "--repo",
- "central=https://repo1.maven.org/maven2",
- "--repo",
- "https://jcenter.bintray.com",
- "com.google.guava:guava:31.1-jre");
-
- // The command should execute successfully (even if dependency resolution might fail)
- assertThat(exitCode >= 0).isTrue();
- }
-
- @Test
- void testInstallCommandWithRepositoryOptions() throws IOException {
- CommandLine cmd = Main.getCommandLine();
- int exitCode =
- cmd.execute(
- "install",
- "--repo",
- "central=https://repo1.maven.org/maven2",
- "com.google.guava:guava:31.1-jre");
-
- // The command should execute successfully (even if dependency resolution might fail)
- assertThat(exitCode >= 0).isTrue();
- }
-
- @Test
- void testPathCommandWithRepositoryOptionsAndAppYml() throws IOException {
- // Create app.yml with repositories
- createAppYmlWithRepositories();
-
- try (TestOutputCapture capture = captureOutput()) {
- CommandLine cmd = Main.getCommandLine();
- int exitCode =
- cmd.execute(
- "path",
- "--repo",
- "jcenter=https://jcenter.bintray.com",
- "com.google.guava:guava:31.1-jre");
-
- // The command should execute (even if dependency resolution might fail)
- assertThat(exitCode >= 0).isTrue();
- }
- }
-
private void createAppYml() throws IOException {
String yamlContent =
"dependencies:\n"
- + " com.github.lalyos:jfiglet: \"0.0.9\"\n"
+ + " fake:dummy: \"1.2.3\"\n"
+ "\n"
+ "actions:\n"
- + " build: \"echo building... .{/}libs{:}{{deps}}\"\n"
- + " test: \"echo testing... .{/}libs{:}{{deps}}\"\n"
- + " run: \"echo running... .{/}libs{:}{{deps}}\"\n"
+ + " build: \"echo building... .{/}libs{:}ext\"\n"
+ + " test: \"echo testing... .{/}libs{:}ext\"\n"
+ + " run: \"echo running... .{/}libs{:}ext\"\n"
+ " hello: \"echo Hello World\"\n";
Files.writeString(tempDir.resolve("app.yml"), yamlContent);
}
private void createAppYmlWithoutActions() throws IOException {
- String yamlContent = "dependencies:\n" + " com.github.lalyos:jfiglet: \"0.0.9\"\n";
+ String yamlContent = "dependencies:\n" + " fake:dummy: \"1.2.3\"\n";
Files.writeString(tempDir.resolve("app.yml"), yamlContent);
}
private void createAppYmlWithoutBuildAction() throws IOException {
String yamlContent =
"dependencies:\n"
- + " com.github.lalyos:jfiglet: \"0.0.9\"\n"
+ + " fake:dummy: \"1.2.3\"\n"
+ "\n"
+ "actions:\n"
+ " test: \"java -cp {{deps}} TestRunner\"\n"
+ " hello: \"echo Hello World\"\n";
Files.writeString(tempDir.resolve("app.yml"), yamlContent);
}
-
- private void createAppYmlWithRepositories() throws IOException {
- String yamlContent =
- "dependencies:\n"
- + " com.github.lalyos:jfiglet: \"0.0.9\"\n"
- + "\n"
- + "repositories:\n"
- + " central: \"https://repo1.maven.org/maven2\"\n"
- + " custom: \"https://my.custom.repo/maven2\"\n"
- + "\n"
- + "actions:\n"
- + " build: \"javac -cp {{deps}} *.java\"\n"
- + " test: \"java -cp {{deps}} TestRunner\"\n";
- Files.writeString(tempDir.resolve("app.yml"), yamlContent);
- }
}