Skip to content

Commit 437bdab

Browse files
committed
feat: implement user configuration file support (issue #107)
Add support for user-level configuration files to set default values for common command-line options. Configuration is loaded from (in priority order): 1. --config CLI option 2. JPM_CONFIG environment variable 3. ~/.config/jpm/config.yml (XDG standard location) 4. ~/.jpmcfg.yml (fallback location) Features: - Configuration for cache, directory, no-links, and repositories options - Home directory expansion (~/ in paths) - Graceful degradation if config file cannot be read - CLI options override config file settings - Repository merging (config repos + CLI repos) - Explicit config path via --config or JPM_CONFIG Implementation: - Created UserConfig class for YAML config parsing - Added ConfigMixin for global --config option (follows VerboseMixin pattern) - Updated DepsMixin to load and use UserConfig - Added test isolation using JPM_CONFIG env var - Updated tests to reflect new usage line with --config option
1 parent 77868ba commit 437bdab

5 files changed

Lines changed: 347 additions & 27 deletions

File tree

src/main/java/org/codejive/jpm/Main.java

Lines changed: 117 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
//DEPS org.yaml:snakeyaml:2.5
66
//DEPS org.jline:jline-console-ui:3.30.6 org.jline:jline-terminal-jni:3.30.6
77
//DEPS org.slf4j:slf4j-api:2.0.17 org.slf4j:slf4j-simple:2.0.17
8-
//SOURCES Jpm.java config/AppInfo.java search/Search.java search/SearchSmoRestImpl.java search/SearchSmoApiImpl.java
8+
//SOURCES Jpm.java config/AppInfo.java config/UserConfig.java
9+
//SOURCES search/Search.java search/SearchSolrRestImpl.java search/SearchSmoApiImpl.java
910
//SOURCES util/CommandsParser.java util/FileUtils.java util/Resolver.java util/ScriptUtils.java util/SyncResult.java
1011
//SOURCES util/Version.java
1112
// spotless:on
@@ -20,6 +21,7 @@
2021
import java.util.*;
2122
import java.util.concurrent.Callable;
2223
import java.util.stream.Collectors;
24+
import org.codejive.jpm.config.UserConfig;
2325
import org.codejive.jpm.search.Search.Backends;
2426
import org.codejive.jpm.util.SyncResult;
2527
import org.codejive.jpm.util.Version;
@@ -63,8 +65,10 @@
6365
public class Main {
6466

6567
static boolean verbose = false;
68+
static Path configFile = null;
6669

6770
@Mixin VerboseMixin verboseMixin;
71+
@Mixin ConfigMixin configMixin;
6872

6973
@Command(
7074
name = "copy",
@@ -75,6 +79,7 @@ public class Main {
7579
+ "Example:\n jpm copy org.apache.httpcomponents:httpclient:4.5.14\n")
7680
static class Copy implements Callable<Integer> {
7781
@Mixin VerboseMixin verboseMixin;
82+
@Mixin ConfigMixin configMixin;
7883
@Mixin QuietMixin quietMixin;
7984
@Mixin ArtifactsMixin artifactsMixin;
8085

@@ -89,8 +94,8 @@ static class Copy implements Callable<Integer> {
8994
public Integer call() throws Exception {
9095
SyncResult stats =
9196
Jpm.builder()
92-
.directory(artifactsMixin.directory)
93-
.noLinks(artifactsMixin.noLinks)
97+
.directory(artifactsMixin.getDirectory())
98+
.noLinks(artifactsMixin.getNoLinks())
9499
.cacheDir(artifactsMixin.getCacheDir())
95100
.build()
96101
.copy(
@@ -115,6 +120,7 @@ public Integer call() throws Exception {
115120
+ "Example:\n jpm search httpclient\n")
116121
static class Search implements Callable<Integer> {
117122
@Mixin VerboseMixin verboseMixin;
123+
@Mixin ConfigMixin configMixin;
118124
@Mixin QuietMixin quietMixin;
119125
@Mixin DepsMixin depsMixin;
120126
@Mixin AppInfoFileMixin appInfoFileMixin;
@@ -162,8 +168,8 @@ public Integer call() throws Exception {
162168
if ("install".equals(artifactAction)) {
163169
SyncResult stats =
164170
Jpm.builder()
165-
.directory(depsMixin.directory)
166-
.noLinks(depsMixin.noLinks)
171+
.directory(depsMixin.getDirectory())
172+
.noLinks(depsMixin.getNoLinks())
167173
.cacheDir(depsMixin.getCacheDir())
168174
.appFile(appInfoFileMixin.appInfoFile)
169175
.build()
@@ -176,8 +182,8 @@ public Integer call() throws Exception {
176182
} else if ("copy".equals(artifactAction)) {
177183
SyncResult stats =
178184
Jpm.builder()
179-
.directory(depsMixin.directory)
180-
.noLinks(depsMixin.noLinks)
185+
.directory(depsMixin.getDirectory())
186+
.noLinks(depsMixin.getNoLinks())
181187
.cacheDir(depsMixin.getCacheDir())
182188
.appFile(appInfoFileMixin.appInfoFile)
183189
.build()
@@ -218,8 +224,8 @@ public Integer call() throws Exception {
218224
String[] search(String artifactPattern) {
219225
try {
220226
return Jpm.builder()
221-
.directory(depsMixin.directory)
222-
.noLinks(depsMixin.noLinks)
227+
.directory(depsMixin.getDirectory())
228+
.noLinks(depsMixin.getNoLinks())
223229
.cacheDir(depsMixin.getCacheDir())
224230
.appFile(appInfoFileMixin.appInfoFile)
225231
.build()
@@ -311,6 +317,7 @@ private static String getSelectedId(
311317
+ "Example:\n jpm install org.apache.httpcomponents:httpclient:4.5.14\n")
312318
static class Install implements Callable<Integer> {
313319
@Mixin VerboseMixin verboseMixin;
320+
@Mixin ConfigMixin configMixin;
314321
@Mixin QuietMixin quietMixin;
315322
@Mixin OptionalArtifactsMixin optionalArtifactsMixin;
316323
@Mixin AppInfoFileMixin appInfoFileMixin;
@@ -319,8 +326,8 @@ static class Install implements Callable<Integer> {
319326
public Integer call() throws Exception {
320327
SyncResult stats =
321328
Jpm.builder()
322-
.directory(optionalArtifactsMixin.directory)
323-
.noLinks(optionalArtifactsMixin.noLinks)
329+
.directory(optionalArtifactsMixin.getDirectory())
330+
.noLinks(optionalArtifactsMixin.getNoLinks())
324331
.cacheDir(optionalArtifactsMixin.getCacheDir())
325332
.appFile(appInfoFileMixin.appInfoFile)
326333
.build()
@@ -343,15 +350,16 @@ public Integer call() throws Exception {
343350
+ "Example:\n jpm path org.apache.httpcomponents:httpclient:4.5.14\n")
344351
static class PrintPath implements Callable<Integer> {
345352
@Mixin VerboseMixin verboseMixin;
353+
@Mixin ConfigMixin configMixin;
346354
@Mixin OptionalArtifactsMixin optionalArtifactsMixin;
347355
@Mixin AppInfoFileMixin appInfoFileMixin;
348356

349357
@Override
350358
public Integer call() throws Exception {
351359
List<Path> files =
352360
Jpm.builder()
353-
.directory(optionalArtifactsMixin.directory)
354-
.noLinks(optionalArtifactsMixin.noLinks)
361+
.directory(optionalArtifactsMixin.getDirectory())
362+
.noLinks(optionalArtifactsMixin.getNoLinks())
355363
.cacheDir(optionalArtifactsMixin.getCacheDir())
356364
.appFile(appInfoFileMixin.appInfoFile)
357365
.build()
@@ -396,6 +404,7 @@ public Integer call() throws Exception {
396404
+ " jpm exec @kotlinc -cp {{deps}} -d out/classes src/main/kotlin/App.kt\n")
397405
static class Exec implements Callable<Integer> {
398406
@Mixin VerboseMixin verboseMixin;
407+
@Mixin ConfigMixin configMixin;
399408
@Mixin DepsMixin depsMixin;
400409
@Mixin QuietMixin quietMixin;
401410
@Mixin AppInfoFileMixin appInfoFileMixin;
@@ -408,8 +417,8 @@ public Integer call() throws Exception {
408417
String cmd = String.join(" ", command);
409418
try {
410419
return Jpm.builder()
411-
.directory(depsMixin.directory)
412-
.noLinks(depsMixin.noLinks)
420+
.directory(depsMixin.getDirectory())
421+
.noLinks(depsMixin.getNoLinks())
413422
.cacheDir(depsMixin.getCacheDir())
414423
.appFile(appInfoFileMixin.appInfoFile)
415424
.verbose(!quietMixin.quiet)
@@ -436,6 +445,7 @@ public Integer call() throws Exception {
436445
+ " jpm do build -a --fresh test -a verbose\n")
437446
static class Do implements Callable<Integer> {
438447
@Mixin VerboseMixin verboseMixin;
448+
@Mixin ConfigMixin configMixin;
439449
@Mixin DepsMixin depsMixin;
440450
@Mixin QuietMixin quietMixin;
441451
@Mixin AppInfoFileMixin appInfoFileMixin;
@@ -467,8 +477,8 @@ public Integer call() throws Exception {
467477
if (list) {
468478
List<String> actionNames =
469479
Jpm.builder()
470-
.directory(depsMixin.directory)
471-
.noLinks(depsMixin.noLinks)
480+
.directory(depsMixin.getDirectory())
481+
.noLinks(depsMixin.getNoLinks())
472482
.cacheDir(depsMixin.getCacheDir())
473483
.appFile(appInfoFileMixin.appInfoFile)
474484
.build()
@@ -513,8 +523,8 @@ public Integer call() throws Exception {
513523
}
514524
int exitCode =
515525
Jpm.builder()
516-
.directory(depsMixin.directory)
517-
.noLinks(depsMixin.noLinks)
526+
.directory(depsMixin.getDirectory())
527+
.noLinks(depsMixin.getNoLinks())
518528
.cacheDir(depsMixin.getCacheDir())
519529
.appFile(appInfoFileMixin.appInfoFile)
520530
.verbose(!quietMixin.quiet)
@@ -535,6 +545,7 @@ public Integer call() throws Exception {
535545

536546
abstract static class DoAlias implements Callable<Integer> {
537547
@Mixin VerboseMixin verboseMixin;
548+
@Mixin ConfigMixin configMixin;
538549
@Mixin DepsMixin depsMixin;
539550
@Mixin AppInfoFileMixin appInfoFileMixin;
540551

@@ -547,8 +558,8 @@ public Integer call() throws Exception {
547558
try {
548559
// Use only unmatched args for pass-through to preserve ordering
549560
return Jpm.builder()
550-
.directory(depsMixin.directory)
551-
.noLinks(depsMixin.noLinks)
561+
.directory(depsMixin.getDirectory())
562+
.noLinks(depsMixin.getNoLinks())
552563
.cacheDir(depsMixin.getCacheDir())
553564
.appFile(appInfoFileMixin.appInfoFile)
554565
.build()
@@ -601,17 +612,19 @@ String actionName() {
601612
}
602613

603614
static class DepsMixin {
615+
// Cached user configuration loaded from ~/.config/jpm/config.yml or ~/.jpmcfg.yml
616+
private transient UserConfig userConfig;
617+
604618
@Option(
605619
names = {"-d", "--directory"},
606-
description = "Directory to copy artifacts to",
607-
defaultValue = "deps")
620+
description = "Directory to copy artifacts to (default: 'deps')")
608621
Path directory;
609622

610623
@Option(
611624
names = {"-L", "--no-links"},
612-
description = "Always copy artifacts, don't try to create symlinks",
613-
defaultValue = "false")
614-
boolean noLinks;
625+
description =
626+
"Always copy artifacts, don't try to create symlinks (default: false)")
627+
Boolean noLinks;
615628

616629
@Option(
617630
names = {"-r", "--repo"},
@@ -625,10 +638,33 @@ static class DepsMixin {
625638
"Directory where downloaded artifacts will be cached (default: value of JPM_CACHE environment variable; whatever is set in Maven's settings.xml or $HOME/.m2/repository")
626639
Path cacheDir;
627640

641+
/**
642+
* Loads and caches the user configuration. Priority: --config option > JPM_CONFIG env var >
643+
* ~/.config/jpm/config.yml > ~/.jpmcfg.yml.
644+
*
645+
* @return The user configuration (never null)
646+
*/
647+
UserConfig getUserConfig() {
648+
if (userConfig == null) {
649+
userConfig = UserConfig.read(Main.configFile);
650+
}
651+
return userConfig;
652+
}
653+
654+
/**
655+
* Returns the cache directory to use. Priority: CLI option > UserConfig > JPM_CACHE env var
656+
* > Maven default.
657+
*
658+
* @return The cache directory path or null to use Maven default
659+
*/
628660
Path getCacheDir() {
629661
if (cacheDir != null) {
630662
return cacheDir;
631663
}
664+
Path userConfigCache = getUserConfig().cache();
665+
if (userConfigCache != null) {
666+
return userConfigCache;
667+
}
632668
String envCache = System.getenv("JPM_CACHE");
633669
if (envCache != null && !envCache.isEmpty()) {
634670
try {
@@ -642,8 +678,53 @@ Path getCacheDir() {
642678
return null;
643679
}
644680

681+
/**
682+
* Returns the directory to use for artifacts. Priority: CLI option > UserConfig > hardcoded
683+
* default ('deps').
684+
*
685+
* @return The directory path
686+
*/
687+
Path getDirectory() {
688+
if (directory != null) {
689+
return directory; // User explicitly set via CLI
690+
}
691+
Path userConfigDir = getUserConfig().directory();
692+
if (userConfigDir != null) {
693+
return userConfigDir; // From user config
694+
}
695+
return Path.of("deps"); // Hardcoded default
696+
}
697+
698+
/**
699+
* Returns whether to disable symlinks. Priority: CLI option > UserConfig > hardcoded
700+
* default (false).
701+
*
702+
* @return true to disable symlinks, false otherwise
703+
*/
704+
boolean getNoLinks() {
705+
if (noLinks != null) {
706+
return noLinks; // User explicitly set via CLI
707+
}
708+
Boolean userConfigNoLinks = getUserConfig().noLinks();
709+
if (userConfigNoLinks != null) {
710+
return userConfigNoLinks; // From user config
711+
}
712+
return false; // Hardcoded default
713+
}
714+
715+
/**
716+
* Returns the repository map. Priority: UserConfig repositories (base) + CLI repositories
717+
* (override).
718+
*
719+
* @return Map of repository name to URL
720+
*/
645721
Map<String, String> getRepositoryMap() {
646722
Map<String, String> repoMap = new HashMap<>();
723+
724+
// Start with UserConfig repositories (lowest priority)
725+
repoMap.putAll(getUserConfig().repositories());
726+
727+
// Add/override with CLI repositories
647728
for (String repo : repositories) {
648729
String name;
649730
String url;
@@ -705,6 +786,16 @@ public void setVerbose(boolean verbose) {
705786
}
706787
}
707788

789+
static class ConfigMixin {
790+
@Option(
791+
names = {"--config"},
792+
description =
793+
"Path to user configuration file (default: JPM_CONFIG environment variable, ~/.config/jpm/config.yml, or ~/.jpmcfg.yml)")
794+
public void setConfigFile(Path config) {
795+
Main.configFile = config;
796+
}
797+
}
798+
708799
static class QuietMixin {
709800
@Option(
710801
names = {"-q", "--quiet"},

0 commit comments

Comments
 (0)