Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions app.yml
Original file line number Diff line number Diff line change
@@ -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 ([email protected])
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
11 changes: 3 additions & 8 deletions src/main/java/org/codejive/jpm/Jpm.java
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,8 @@ public SyncResult install(String[] artifactNames, Map<String, String> extraRepos
List<Path> 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;
Expand Down Expand Up @@ -254,7 +249,7 @@ private static String[] getArtifacts(String[] artifactNames, AppInfo appInfo) {
}

private Map<String, String> getRepositories(Map<String, String> extraRepos, AppInfo appInfo) {
Map<String, String> repos = new HashMap<>(appInfo.repositories);
Map<String, String> repos = new HashMap<>(appInfo.repositories());
repos.putAll(extraRepos);
return repos;
}
Expand Down
172 changes: 136 additions & 36 deletions src/main/java/org/codejive/jpm/config/AppInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,22 +20,64 @@
public class AppInfo {
private Map<String, Object> yaml = new LinkedHashMap<>();

public Map<String, String> dependencies = new LinkedHashMap<>();
public Map<String, String> repositories = new LinkedHashMap<>();
public Map<String, String> actions = new LinkedHashMap<>();
private final List<String> dependencies = new ArrayList<>();
private final Map<String, String> repositories = new LinkedHashMap<>();
private final Map<String, String> 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<String> authors() {
if (yaml.get("authors") instanceof List) {
return (List<String>) yaml.get("authors");
}
return null;
}

@SuppressWarnings("unchecked")
public List<String> contributors() {
if (yaml.get("contributors") instanceof List) {
return (List<String>) yaml.get("contributors");
}
return null;
}

public List<String> dependencies() {
return dependencies;
}

public Map<String, String> repositories() {
return repositories;
}

public Map<String, String> 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);
}

/**
Expand All @@ -57,30 +101,63 @@ public java.util.Set<String> 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<String, Object> deps = (Map<String, Object>) appInfo.yaml.get("dependencies");
for (Map.Entry<String, Object> 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<String, Object> deps = (Map<String, Object>) appInfo.yaml.get("dependencies");
for (Map.Entry<String, Object> entry : deps.entrySet()) {
appInfo.dependencies.add(entry.getKey() + ":" + entry.getValue());
}
} else if (appInfo.yaml.get("dependencies") instanceof List) {
List<String> deps = (List<String>) appInfo.yaml.get("dependencies");
appInfo.dependencies.addAll(deps);
}
}
// Parse repositories section
Expand Down Expand Up @@ -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<String, Object>) (Map) appInfo.dependencies);
if (!appInfo.repositories.isEmpty()) {
appInfo.yaml.put("repositories", (Map<String, Object>) (Map) appInfo.repositories);
} else {
appInfo.yaml.remove("repositories");
}
if (!appInfo.actions.isEmpty()) {
appInfo.yaml.put("actions", (Map<String, Object>) (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<String, Object>) (Map) appInfo.repositories);
} else {
appInfo.yaml.remove("repositories");
}
if (!appInfo.actions.isEmpty()) {
appInfo.yaml.put("actions", (Map<String, Object>) (Map) appInfo.actions);
} else {
appInfo.yaml.remove("actions");
}
yaml.dump(appInfo.yaml, out);
}
}
36 changes: 36 additions & 0 deletions src/test/java/org/codejive/jpm/config/AppInfoLegacyTest.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading