Skip to content

Commit 80ffccf

Browse files
Feature/config loading from classpath (#72)
* Added new methods loadFromClassPath, loadOnceWithClasspathFallback and createMapperForName. Modified createMapperFor-method to work with createMapperForName * Updated main to use new method loadOnceWithClasspathFallback * Added new test: fallback_to_classpath_when_external_file_missing() * reworked loadOnceWithCLasspathFallback to adress external file deleted between Files.exists and load() silently bypasses the classpath fallback. Removed unused code from main. * Replaced new YAMLFactory() with YAMLFactory.builder().build() after feedback from coderabbit. * Uppdated main to run both loadOnceWithClasspathFallback and cli. * Added null preconditions after feedback from coderabbit and removed unused import.
1 parent bc93313 commit 80ffccf

4 files changed

Lines changed: 76 additions & 7 deletions

File tree

src/main/java/org/example/App.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ public class App {
1111
private static final String PORT_FLAG = "--port";
1212

1313
public static void main(String[] args) {
14-
Path configPath = Path.of("src/main/resources/application.yml");
1514

16-
AppConfig appConfig = ConfigLoader.loadOnce(configPath);
15+
AppConfig appConfig = ConfigLoader.loadOnceWithClasspathFallback(
16+
Path.of("application.yml"),
17+
"application.yml"
18+
);
1719

1820
int port = resolvePort(args, appConfig.server().port());
1921

src/main/java/org/example/config/ConfigLoader.java

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import tools.jackson.databind.ObjectMapper;
44
import tools.jackson.databind.json.JsonMapper;
5-
import tools.jackson.dataformat.yaml.YAMLFactory;
65
import tools.jackson.dataformat.yaml.YAMLMapper;
76

87
import java.io.InputStream;
@@ -27,6 +26,32 @@ public static AppConfig loadOnce(Path configPath) {
2726
}
2827
}
2928

29+
public static AppConfig loadOnceWithClasspathFallback(Path externalPath, String classpathResourceName) {
30+
Objects.requireNonNull(externalPath, "externalPath");
31+
Objects.requireNonNull(classpathResourceName, "classpathResourceName");
32+
if (cached != null) return cached;
33+
34+
synchronized (ConfigLoader.class) {
35+
if (cached == null){
36+
AppConfig base;
37+
try (InputStream ext = Files.newInputStream(externalPath)) {
38+
ObjectMapper objectMapper = createMapperFor(externalPath);
39+
AppConfig config = objectMapper.readValue(ext, AppConfig.class);
40+
base = config == null ? AppConfig.defaults() : config;
41+
42+
} catch (java.nio.file.NoSuchFileException ignored) {
43+
base = loadFromClasspath(classpathResourceName);
44+
45+
} catch (Exception e) {
46+
throw new IllegalStateException("failed to read config file " + externalPath.toAbsolutePath(), e);
47+
}
48+
49+
cached = base.withDefaultsApplied();
50+
}
51+
return cached;
52+
}
53+
}
54+
3055
public static AppConfig get(){
3156
if (cached == null){
3257
throw new IllegalStateException("Config not loaded. call ConfigLoader.loadOnce(...) at startup.");
@@ -52,17 +77,41 @@ public static AppConfig load(Path configPath) {
5277
}
5378
}
5479

55-
private static ObjectMapper createMapperFor(Path configPath) {
56-
String name = configPath.getFileName().toString().toLowerCase();
80+
public static AppConfig loadFromClasspath(String classpathResourceName) {
81+
Objects.requireNonNull(classpathResourceName, "classpathResourceName");
82+
83+
try (InputStream stream = ConfigLoader.class.getClassLoader().getResourceAsStream(classpathResourceName)) {
84+
if (stream == null) {
85+
return AppConfig.defaults();
86+
}
87+
88+
ObjectMapper objectMapper = createMapperForName(classpathResourceName);
89+
90+
AppConfig config = objectMapper.readValue(stream, AppConfig.class);
91+
return config == null ? AppConfig.defaults() : config;
92+
} catch (Exception e){
93+
throw new IllegalStateException("failed to read config file from classpath: " + classpathResourceName, e);
94+
}
95+
}
96+
97+
private static ObjectMapper createMapperForName(String fileName) {
98+
String name = fileName.toLowerCase();
5799

58100
if (name.endsWith(".yml") || name.endsWith(".yaml")) {
59-
return YAMLMapper.builder(new YAMLFactory()).build();
101+
return YAMLMapper.builder().build();
60102

61103
} else if (name.endsWith(".json")) {
62104
return JsonMapper.builder().build();
63105
} else {
64-
return YAMLMapper.builder(new YAMLFactory()).build();
106+
return YAMLMapper.builder().build();
65107
}
108+
109+
}
110+
111+
private static ObjectMapper createMapperFor(Path configPath) {
112+
String name = configPath.getFileName().toString();
113+
114+
return createMapperForName(name);
66115
}
67116

68117
public static void resetForTests() {

src/test/java/org/example/config/ConfigLoaderTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,17 @@ void invalid_port_should_Throw_Exception () throws Exception {
137137

138138
assertThatThrownBy(() -> ConfigLoader.loadOnce(configFile))
139139
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Invalid port number");
140+
}
141+
142+
@Test
143+
@DisplayName("missing external file should fallback to classpath")
144+
void fallback_to_classpath_when_external_file_missing(){
145+
AppConfig appConfig = ConfigLoader.loadOnceWithClasspathFallback(tempDir.resolve("missing.yml"),"application.yml");
146+
147+
assertThat(appConfig.server().port()).isEqualTo(3030);
148+
149+
150+
151+
140152
}
141153
}

src/test/resources/application.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
server:
2+
port: 3030
3+
rootDir: ./from-classpath
4+
5+
logging:
6+
level: DEBUG

0 commit comments

Comments
 (0)