diff --git a/app.yml b/app.yml index e2d0160..7f9f746 100644 --- a/app.yml +++ b/app.yml @@ -1,18 +1,38 @@ -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" - org.jline:jline-terminal-jni: "3.30.5" - org.slf4j:slf4j-api: "2.0.17" - org.slf4j:slf4j-simple: "2.0.17" +name: jpm +description: A simple command line tool to manage Maven dependencies for Java projects + that are not using build systems like Maven or Gradle +documentation: | + # jpm - Java Package Manager + + A simple command line tool to manage Maven dependencies for Java projects that are not using build systems like Maven + or Gradle. + It takes inspiration from Node's npm but is more focused on managing dependencies and + is _not_ a build tool. Keep using Maven and Gradle for that. This tool is ideal for those who want to compile and + run Java code directly without making their lives difficult the moment they want to start using dependencies. +authors: +- Tako Schotanus (tako@codejive.org) +contributors: +- copilot +links: + homepage: https://github.com/codejive/java-jpm + repository: https://github.com/codejive/java-jpm + documentation: https://github.com/codejive/java-jpm/edit/main/README.md +java: 11 +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 + org.jline:jline-terminal-jni: 3.30.5 + org.slf4j:slf4j-api: 2.0.17 + org.slf4j:slf4j-simple: 2.0.17 actions: - clean: "./mvnw clean" - build: "./mvnw spotless:apply package -DskipTests" - run: "./target/binary/bin/jpm" - test: "./mvnw test" - buildj: "javac -cp {{deps}} -d classes --source-path src/main/java src/main/java/org/codejive/jpm/Main.java" - runj: "java -cp classes:{{deps}} org.codejive.jpm.Main" + clean: ./mvnw clean + build: ./mvnw spotless:apply package -DskipTests + run: ./target/binary/bin/jpm + test: ./mvnw test + buildj: javac -cp {{deps}} -d classes --source-path src/main/java src/main/java/org/codejive/jpm/Main.java + runj: java -cp classes:{{deps}} org.codejive.jpm.Main diff --git a/src/main/java/org/codejive/jpm/Jpm.java b/src/main/java/org/codejive/jpm/Jpm.java index 856c0bc..72f8bc2 100644 --- a/src/main/java/org/codejive/jpm/Jpm.java +++ b/src/main/java/org/codejive/jpm/Jpm.java @@ -186,13 +186,8 @@ public SyncResult install(String[] artifactNames, Map extraRepos List files = Resolver.create(artifacts, repos).resolvePaths(); SyncResult stats = FileUtils.syncArtifacts(files, directory, noLinks, true); if (artifactNames.length > 0) { - for (String dep : artifactNames) { - int p = dep.lastIndexOf(':'); - String name = dep.substring(0, p); - String version = dep.substring(p + 1); - appInfo.dependencies.put(name, version); - } - appInfo.repositories.putAll(repos); + appInfo.dependencies().addAll(Arrays.asList(artifactNames)); + appInfo.repositories().putAll(repos); AppInfo.write(appInfo); } return stats; @@ -254,7 +249,7 @@ private static String[] getArtifacts(String[] artifactNames, AppInfo appInfo) { } private Map getRepositories(Map extraRepos, AppInfo appInfo) { - Map repos = new HashMap<>(appInfo.repositories); + Map repos = new HashMap<>(appInfo.repositories()); repos.putAll(extraRepos); return repos; } diff --git a/src/main/java/org/codejive/jpm/config/AppInfo.java b/src/main/java/org/codejive/jpm/config/AppInfo.java index 3b182a6..216d34a 100644 --- a/src/main/java/org/codejive/jpm/config/AppInfo.java +++ b/src/main/java/org/codejive/jpm/config/AppInfo.java @@ -6,7 +6,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; @@ -18,22 +20,64 @@ public class AppInfo { private Map yaml = new LinkedHashMap<>(); - public Map dependencies = new LinkedHashMap<>(); - public Map repositories = new LinkedHashMap<>(); - public Map actions = new LinkedHashMap<>(); + private final List dependencies = new ArrayList<>(); + private final Map repositories = new LinkedHashMap<>(); + private final Map actions = new LinkedHashMap<>(); /** The official name of the app.yml file. */ public static final String APP_INFO_FILE = "app.yml"; + public String name() { + return (String) yaml.get("name"); + } + + public String description() { + return (String) yaml.get("description"); + } + + public String documentation() { + return (String) yaml.get("documentation"); + } + + public String java() { + return (String) yaml.get("java"); + } + + @SuppressWarnings("unchecked") + public List authors() { + if (yaml.get("authors") instanceof List) { + return (List) yaml.get("authors"); + } + return null; + } + + @SuppressWarnings("unchecked") + public List contributors() { + if (yaml.get("contributors") instanceof List) { + return (List) yaml.get("contributors"); + } + return null; + } + + public List dependencies() { + return dependencies; + } + + public Map repositories() { + return repositories; + } + + public Map actions() { + return actions; + } + /** * Returns the dependencies as an array of strings in the format "groupId:artifactId:version". * * @return An array of strings */ public String[] getDependencyGAVs() { - return dependencies.entrySet().stream() - .map(e -> e.getKey() + ":" + e.getValue()) - .toArray(String[]::new); + return dependencies.toArray(String[]::new); } /** @@ -57,30 +101,63 @@ public java.util.Set getActionNames() { /** * Reads the app.yml file in the current directory and returns its content as an AppInfo object. + * If the file does not exist, an empty AppInfo object is returned. * * @return An instance of AppInfo * @throws IOException if an error occurred while reading or parsing the file */ @SuppressWarnings("unchecked") public static AppInfo read() throws IOException { - Path prjJson = Paths.get(System.getProperty("user.dir"), APP_INFO_FILE); - AppInfo appInfo = new AppInfo(); - if (Files.isRegularFile(prjJson)) { - try (Reader in = Files.newBufferedReader(prjJson)) { - Yaml yaml = new Yaml(); - appInfo.yaml = yaml.load(in); + Path appInfoFile = Paths.get(System.getProperty("user.dir"), APP_INFO_FILE); + return read(appInfoFile); + } + + /** + * Reads the app.yml file in the current directory and returns its content as an AppInfo object. + * If the file does not exist, an empty AppInfo object is returned. + * + * @param appInfoFile The path to the app.yml file + * @return An instance of AppInfo + * @throws IOException if an error occurred while reading or parsing the file + */ + @SuppressWarnings("unchecked") + public static AppInfo read(Path appInfoFile) throws IOException { + if (Files.isRegularFile(appInfoFile)) { + try (Reader in = Files.newBufferedReader(appInfoFile)) { + return read(in); } } + return new AppInfo(); + } + + /** + * Reads the app.yml from the given Reader and returns its content as an AppInfo object. + * + * @param in The Reader to read the app.yml content from + * @return An instance of AppInfo + */ + @SuppressWarnings("unchecked") + public static AppInfo read(Reader in) { + AppInfo appInfo = new AppInfo(); + Yaml yaml = new Yaml(); + appInfo.yaml = yaml.load(in); // Ensure yaml is never null if (appInfo.yaml == null) { appInfo.yaml = new LinkedHashMap<>(); } + // We now take any known information from the Yaml map and transfer it to their + // respective fields in the AppInfo object, leaving unknown information untouched // WARNING awful code ahead - if (appInfo.yaml.containsKey("dependencies") - && appInfo.yaml.get("dependencies") instanceof Map) { - Map deps = (Map) appInfo.yaml.get("dependencies"); - for (Map.Entry entry : deps.entrySet()) { - appInfo.dependencies.put(entry.getKey(), entry.getValue().toString()); + // Parse dependencies section + if (appInfo.yaml.containsKey("dependencies")) { + if (appInfo.yaml.get("dependencies") instanceof Map) { + Map deps = (Map) appInfo.yaml.get("dependencies"); + for (Map.Entry entry : deps.entrySet()) { + appInfo.dependencies.add(entry.getKey() + ":" + entry.getValue()); + } + } else if (appInfo.yaml.get("dependencies") instanceof List) { + List deps = (List) appInfo.yaml.get("dependencies"); + appInfo.dependencies.addAll(deps); } } // Parse repositories section @@ -109,25 +186,48 @@ public static AppInfo read() throws IOException { */ @SuppressWarnings("unchecked") public static void write(AppInfo appInfo) throws IOException { - Path prjJson = Paths.get(System.getProperty("user.dir"), APP_INFO_FILE); - try (Writer out = Files.newBufferedWriter(prjJson)) { - DumperOptions dopts = new DumperOptions(); - dopts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - dopts.setPrettyFlow(true); - Yaml yaml = new Yaml(dopts); - // WARNING awful code ahead - appInfo.yaml.put("dependencies", (Map) (Map) appInfo.dependencies); - if (!appInfo.repositories.isEmpty()) { - appInfo.yaml.put("repositories", (Map) (Map) appInfo.repositories); - } else { - appInfo.yaml.remove("repositories"); - } - if (!appInfo.actions.isEmpty()) { - appInfo.yaml.put("actions", (Map) (Map) appInfo.actions); - } else { - appInfo.yaml.remove("actions"); - } - yaml.dump(appInfo.yaml, out); + Path appInfoFile = Paths.get(System.getProperty("user.dir"), APP_INFO_FILE); + write(appInfo, appInfoFile); + } + + /** + * Writes the AppInfo object to the given path. + * + * @param appInfo The AppInfo object to write + * @param appInfoFile The path to write the app.yml file to + * @throws IOException if an error occurred while writing the file + */ + @SuppressWarnings("unchecked") + public static void write(AppInfo appInfo, Path appInfoFile) throws IOException { + try (Writer out = Files.newBufferedWriter(appInfoFile)) { + write(appInfo, out); + } + } + + /** + * Writes the AppInfo object to the given Writer. + * + * @param appInfo The AppInfo object to write + * @param out The Writer to write to + */ + @SuppressWarnings("unchecked") + public static void write(AppInfo appInfo, Writer out) { + DumperOptions dopts = new DumperOptions(); + dopts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + dopts.setPrettyFlow(true); + Yaml yaml = new Yaml(dopts); + // WARNING awful code ahead + appInfo.yaml.put("dependencies", appInfo.dependencies); + if (!appInfo.repositories.isEmpty()) { + appInfo.yaml.put("repositories", (Map) (Map) appInfo.repositories); + } else { + appInfo.yaml.remove("repositories"); + } + if (!appInfo.actions.isEmpty()) { + appInfo.yaml.put("actions", (Map) (Map) appInfo.actions); + } else { + appInfo.yaml.remove("actions"); } + yaml.dump(appInfo.yaml, out); } } diff --git a/src/test/java/org/codejive/jpm/config/AppInfoLegacyTest.java b/src/test/java/org/codejive/jpm/config/AppInfoLegacyTest.java new file mode 100644 index 0000000..d36b5d1 --- /dev/null +++ b/src/test/java/org/codejive/jpm/config/AppInfoLegacyTest.java @@ -0,0 +1,36 @@ +package org.codejive.jpm.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Tests for AppInfo class, focusing on action parsing and management. */ +class AppInfoLegacyTest { + + @TempDir Path tempDir; + + @Test + void testUpdateLegacyDeps() throws IOException { + // Create a test app.yml file with legacy dependencies map + Path appYmlPath = tempDir.resolve("app.yml"); + String yamlContent = "dependencies:\n" + " com.example:test-lib: \"1.0.0\"\n"; + Files.writeString(appYmlPath, yamlContent); + + AppInfo appInfo = AppInfo.read(appYmlPath); + + // Test dependencies are still parsed correctly + assertThat(appInfo.dependencies()).hasSize(1); + assertThat(appInfo.dependencies()).contains("com.example:test-lib:1.0.0"); + + AppInfo.write(appInfo, appYmlPath); + + // Verify the file now uses new dependencies list format + String updatedContent = Files.readString(appYmlPath); + assertThat(updatedContent).doesNotContain("com.example:test-lib: \"1.0.0\""); + assertThat(updatedContent).contains("- com.example:test-lib:1.0.0"); + } +} diff --git a/src/test/java/org/codejive/jpm/config/AppInfoTest.java b/src/test/java/org/codejive/jpm/config/AppInfoTest.java index 0a96661..dd57d2d 100644 --- a/src/test/java/org/codejive/jpm/config/AppInfoTest.java +++ b/src/test/java/org/codejive/jpm/config/AppInfoTest.java @@ -20,7 +20,7 @@ void testReadAppInfoWithActions() throws IOException { Path appYmlPath = tempDir.resolve("app.yml"); String yamlContent = "dependencies:\n" - + " com.example:test-lib: \"1.0.0\"\n" + + " - com.example:test-lib:1.0.0\n" + "\n" + "actions:\n" + " build: \"javac -cp {{deps}} *.java\"\n" @@ -51,8 +51,8 @@ void testReadAppInfoWithActions() throws IOException { assertThat(appInfo.getAction("nonexistent")).isNull(); // Test dependencies are still parsed correctly - assertThat(appInfo.dependencies).hasSize(1); - assertThat(appInfo.dependencies).containsEntry("com.example:test-lib", "1.0.0"); + assertThat(appInfo.dependencies()).hasSize(1); + assertThat(appInfo.dependencies()).contains("com.example:test-lib:1.0.0"); } finally { System.setProperty("user.dir", originalDir); } @@ -62,7 +62,7 @@ void testReadAppInfoWithActions() throws IOException { void testReadAppInfoWithoutActions() throws IOException { // Create a test app.yml file without actions Path appYmlPath = tempDir.resolve("app.yml"); - String yamlContent = "dependencies:\n" + " com.example:test-lib: \"1.0.0\"\n"; + String yamlContent = "dependencies:\n" + " - com.example:test-lib:1.0.0\n"; Files.writeString(appYmlPath, yamlContent); String originalDir = System.getProperty("user.dir"); @@ -76,7 +76,7 @@ void testReadAppInfoWithoutActions() throws IOException { assertThat(appInfo.getAction("build")).isNull(); // Test dependencies are still parsed correctly - assertThat(appInfo.dependencies).hasSize(1); + assertThat(appInfo.dependencies()).hasSize(1); } finally { System.setProperty("user.dir", originalDir); } @@ -93,7 +93,7 @@ void testReadEmptyAppInfo() throws IOException { // Test no actions and no dependencies assertThat(appInfo.getActionNames()).isEmpty(); - assertThat(appInfo.dependencies).isEmpty(); + assertThat(appInfo.dependencies()).isEmpty(); assertThat(appInfo.getAction("build")).isNull(); } finally { System.setProperty("user.dir", originalDir); @@ -103,9 +103,9 @@ void testReadEmptyAppInfo() throws IOException { @Test void testWriteAppInfoWithActions() throws IOException { AppInfo appInfo = new AppInfo(); - appInfo.dependencies.put("com.example:test-lib", "1.0.0"); - appInfo.actions.put("build", "javac -cp {{deps}} *.java"); - appInfo.actions.put("test", "java -cp {{deps}} TestRunner"); + appInfo.dependencies().add("com.example:test-lib:1.0.0"); + appInfo.actions().put("build", "javac -cp {{deps}} *.java"); + appInfo.actions().put("test", "java -cp {{deps}} TestRunner"); String originalDir = System.getProperty("user.dir"); System.setProperty("user.dir", tempDir.toString()); @@ -122,7 +122,7 @@ void testWriteAppInfoWithActions() throws IOException { assertThat(readBack.getAction("build")).isEqualTo("javac -cp {{deps}} *.java"); assertThat(readBack.getAction("test")).isEqualTo("java -cp {{deps}} TestRunner"); assertThat(readBack.getActionNames()).hasSize(2); - assertThat(readBack.dependencies).hasSize(1); + assertThat(readBack.dependencies()).hasSize(1); } finally { System.setProperty("user.dir", originalDir); } @@ -133,7 +133,7 @@ void testAppInfoWithComplexActions() throws IOException { Path appYmlPath = tempDir.resolve("app.yml"); String yamlContent = "dependencies:\n" - + " com.example:test-lib: \"1.0.0\"\n" + + " - com.example:test-lib:1.0.0\n" + "\n" + "actions:\n" + " complex: \"java -cp {{deps}} -Dprop=value MainClass arg1 arg2\"\n" @@ -166,7 +166,7 @@ void testReadAppInfoWithRepositories() throws IOException { Path appYmlPath = tempDir.resolve("app.yml"); String yamlContent = "dependencies:\n" - + " com.example:test-lib: \"1.0.0\"\n" + + " - com.example:test-lib:1.0.0\n" + "\n" + "repositories:\n" + " central: \"https://repo1.maven.org/maven2\"\n" @@ -181,15 +181,15 @@ void testReadAppInfoWithRepositories() throws IOException { AppInfo appInfo = AppInfo.read(); // Test repository retrieval - assertThat(appInfo.repositories).hasSize(3); - assertThat(appInfo.repositories) + assertThat(appInfo.repositories()).hasSize(3); + assertThat(appInfo.repositories()) .containsEntry("central", "https://repo1.maven.org/maven2") .containsEntry("jcenter", "https://jcenter.bintray.com") .containsEntry("custom", "https://my.custom.repo/maven2"); // Test dependencies are still parsed correctly - assertThat(appInfo.dependencies).hasSize(1); - assertThat(appInfo.dependencies).containsEntry("com.example:test-lib", "1.0.0"); + assertThat(appInfo.dependencies()).hasSize(1); + assertThat(appInfo.dependencies()).contains("com.example:test-lib:1.0.0"); } finally { System.setProperty("user.dir", originalDir); } @@ -199,7 +199,7 @@ void testReadAppInfoWithRepositories() throws IOException { void testReadAppInfoWithoutRepositories() throws IOException { // Create a test app.yml file without repositories Path appYmlPath = tempDir.resolve("app.yml"); - String yamlContent = "dependencies:\n" + " com.example:test-lib: \"1.0.0\"\n"; + String yamlContent = "dependencies:\n" + " - com.example:test-lib:1.0.0\n"; Files.writeString(appYmlPath, yamlContent); String originalDir = System.getProperty("user.dir"); @@ -209,10 +209,10 @@ void testReadAppInfoWithoutRepositories() throws IOException { AppInfo appInfo = AppInfo.read(); // Test no repositories - assertThat(appInfo.repositories).isEmpty(); + assertThat(appInfo.repositories()).isEmpty(); // Test dependencies are still parsed correctly - assertThat(appInfo.dependencies).hasSize(1); + assertThat(appInfo.dependencies()).hasSize(1); } finally { System.setProperty("user.dir", originalDir); } @@ -221,9 +221,9 @@ void testReadAppInfoWithoutRepositories() throws IOException { @Test void testWriteAppInfoWithRepositories() throws IOException { AppInfo appInfo = new AppInfo(); - appInfo.dependencies.put("com.example:test-lib", "1.0.0"); - appInfo.repositories.put("central", "https://repo1.maven.org/maven2"); - appInfo.repositories.put("custom", "https://my.custom.repo/maven2"); + appInfo.dependencies().add("com.example:test-lib:1.0.0"); + appInfo.repositories().put("central", "https://repo1.maven.org/maven2"); + appInfo.repositories().put("custom", "https://my.custom.repo/maven2"); String originalDir = System.getProperty("user.dir"); System.setProperty("user.dir", tempDir.toString()); @@ -237,11 +237,11 @@ void testWriteAppInfoWithRepositories() throws IOException { // Read it back and verify AppInfo readBack = AppInfo.read(); - assertThat(readBack.repositories).hasSize(2); - assertThat(readBack.repositories) + assertThat(readBack.repositories()).hasSize(2); + assertThat(readBack.repositories()) .containsEntry("central", "https://repo1.maven.org/maven2") .containsEntry("custom", "https://my.custom.repo/maven2"); - assertThat(readBack.dependencies).hasSize(1); + assertThat(readBack.dependencies()).hasSize(1); } finally { System.setProperty("user.dir", originalDir); } @@ -250,7 +250,7 @@ void testWriteAppInfoWithRepositories() throws IOException { @Test void testWriteAppInfoWithoutRepositories() throws IOException { AppInfo appInfo = new AppInfo(); - appInfo.dependencies.put("com.example:test-lib", "1.0.0"); + appInfo.dependencies().add("com.example:test-lib:1.0.0"); // No repositories added String originalDir = System.getProperty("user.dir"); @@ -261,8 +261,8 @@ void testWriteAppInfoWithoutRepositories() throws IOException { // Read it back and verify repositories section is not present AppInfo readBack = AppInfo.read(); - assertThat(readBack.repositories).isEmpty(); - assertThat(readBack.dependencies).hasSize(1); + assertThat(readBack.repositories()).isEmpty(); + assertThat(readBack.dependencies()).hasSize(1); // Also verify the YAML content doesn't contain repositories section String content = Files.readString(tempDir.resolve("app.yml")); @@ -277,7 +277,7 @@ void testAppInfoWithComplexRepositoriesAndActions() throws IOException { Path appYmlPath = tempDir.resolve("app.yml"); String yamlContent = "dependencies:\n" - + " com.example:test-lib: \"1.0.0\"\n" + + " - com.example:test-lib:1.0.0\n" + "\n" + "repositories:\n" + " central: \"https://repo1.maven.org/maven2\"\n" @@ -294,11 +294,11 @@ void testAppInfoWithComplexRepositoriesAndActions() throws IOException { AppInfo appInfo = AppInfo.read(); // Test all sections are parsed correctly - assertThat(appInfo.dependencies).hasSize(1); - assertThat(appInfo.repositories).hasSize(2); + assertThat(appInfo.dependencies()).hasSize(1); + assertThat(appInfo.repositories()).hasSize(2); assertThat(appInfo.getActionNames()).hasSize(1); - assertThat(appInfo.repositories) + assertThat(appInfo.repositories()) .containsEntry("central", "https://repo1.maven.org/maven2") .containsEntry( "sonatype-snapshots",