Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit ba68f3f

Browse files
committed
env.resolve fail on missing properties must be configurable fix jooby-project#578
1 parent ba480a6 commit ba68f3f

File tree

5 files changed

+192
-108
lines changed

5 files changed

+192
-108
lines changed

jooby-assets/src/main/java/org/jooby/assets/Props.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.regex.Pattern;
2525

2626
import org.jooby.Env;
27+
import org.jooby.Env.Resolver;
2728
import org.jooby.MediaType;
2829

2930
import com.typesafe.config.Config;
@@ -62,8 +63,10 @@
6263
* dev: [props]
6364
* dist: [props]
6465
* }
66+
*
6567
* props {
6668
* delims: [{{, }}]
69+
* ignoreMissing: true
6770
* }
6871
* }
6972
* </pre>
@@ -77,6 +80,7 @@ public class Props extends AssetProcessor {
7780

7881
{
7982
set("delims", Arrays.asList("${", "}"));
83+
set("ignoreMissing", false);
8084
}
8185

8286
@Override
@@ -90,7 +94,13 @@ public String process(final String filename, final String source, final Config c
9094
try {
9195
Env env = Env.DEFAULT.build(conf);
9296
List<String> delims = get("delims");
93-
return env.resolve(source, delims.get(0), delims.get(1));
97+
Resolver resolver = env.resolver();
98+
boolean ignoreMissing = get("ignoreMissing");
99+
if (ignoreMissing) {
100+
resolver.ignoreMissing();
101+
}
102+
resolver.delimiters(delims.get(0), delims.get(1));
103+
return resolver.resolve(source);
94104
} catch (Exception cause) {
95105
int line = -1;
96106
int column = -1;

jooby-assets/src/test/java/org/jooby/assets/PropsTest.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,31 @@ public void shouldSupportCustomDelimiters() throws Exception {
3737
.empty().withValue("app.url", ConfigValueFactory.fromAnyRef("http://foo.com"))));
3838
}
3939

40+
@Test
41+
public void ignoreMissingProps() throws Exception {
42+
assertEquals("$.ajax(${app.url});",
43+
new Props()
44+
.set("ignoreMissing", true)
45+
.process("/j.s", "$.ajax(${app.url});", ConfigFactory.empty()));
46+
}
47+
4048
@Test
4149
public void missingProp() throws Exception {
4250
try {
4351
new Props().process("/j.s", "$.ajax(${cpath}/service);", ConfigFactory.empty());
4452
fail();
4553
} catch (AssetException ex) {
46-
assertEquals("\t/j.s:1:8: No configuration setting found for key 'cpath' at 1:8", ex.getMessage());
54+
assertEquals("\t/j.s:1:8: No configuration setting found for key 'cpath' at 1:8",
55+
ex.getMessage());
4756
assertEquals("No configuration setting found for key 'cpath' at 1:8", ex.get());
4857
}
4958

5059
try {
5160
new Props().process("/j.s", "$.ajax(\n\n ${cpath}/service);", ConfigFactory.empty());
5261
fail();
5362
} catch (AssetException ex) {
54-
assertEquals("\t/j.s:3:4: No configuration setting found for key 'cpath' at 3:4", ex.getMessage());
63+
assertEquals("\t/j.s:3:4: No configuration setting found for key 'cpath' at 3:4",
64+
ex.getMessage());
5565
assertEquals("No configuration setting found for key 'cpath' at 3:4", ex.get());
5666
}
5767
}

jooby/src/main/java/org/jooby/Env.java

Lines changed: 131 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
*/
1919
package org.jooby;
2020

21-
import static com.google.common.base.Preconditions.checkArgument;
2221
import static java.util.Objects.requireNonNull;
2322

2423
import java.util.Arrays;
@@ -38,11 +37,11 @@
3837
import java.util.function.Supplier;
3938

4039
import com.google.common.base.Splitter;
41-
import com.google.common.base.Strings;
4240
import com.google.common.collect.ImmutableList;
4341
import com.google.inject.Key;
4442
import com.google.inject.name.Names;
4543
import com.typesafe.config.Config;
44+
import com.typesafe.config.ConfigFactory;
4645

4746
import javaslang.API;
4847
import javaslang.control.Option;
@@ -70,6 +69,130 @@
7069
*/
7170
public interface Env extends LifeCycle {
7271

72+
/**
73+
* Template literal implementation, replaces <code>${expression}</code> from a String using a
74+
* {@link Config} object.
75+
*
76+
* @author edgar
77+
*/
78+
class Resolver {
79+
private String startDelim = "${";
80+
81+
private String endDelim = "}";
82+
83+
private Config source;
84+
85+
private boolean ignoreMissing;
86+
87+
/**
88+
* Set property source.
89+
*
90+
* @param source Source.
91+
* @return This resolver.
92+
*/
93+
public Resolver source(final Map<String, Object> source) {
94+
this.source = ConfigFactory.parseMap(source);
95+
return this;
96+
}
97+
98+
/**
99+
* Set property source.
100+
*
101+
* @param source Source.
102+
* @return This resolver.
103+
*/
104+
public Resolver source(final Config source) {
105+
this.source = source;
106+
return this;
107+
}
108+
109+
/**
110+
* Set start and end delimiters.
111+
*
112+
* @param start Start delimiter.
113+
* @param end End delimiter.
114+
* @return This resolver.
115+
*/
116+
public Resolver delimiters(final String start, final String end) {
117+
this.startDelim = requireNonNull(start, "Start delimiter required.");
118+
this.endDelim = requireNonNull(end, "End delmiter required.");
119+
return this;
120+
}
121+
122+
/**
123+
* Ignore missing property replacement and leave the expression untouch.
124+
*
125+
* @return This resolver.
126+
*/
127+
public Resolver ignoreMissing() {
128+
this.ignoreMissing = true;
129+
return this;
130+
}
131+
132+
/**
133+
* Returns a string with all substitutions (the <code>${foo.bar}</code> syntax,
134+
* see <a href="https://github.com/typesafehub/config/blob/master/HOCON.md">the
135+
* spec</a>) resolved. Substitutions are looked up using the <code>source</code> param as the
136+
* root object, that is, a substitution <code>${foo.bar}</code> will be replaced with
137+
* the result of <code>getValue("foo.bar")</code>.
138+
*
139+
* @param text Text to process.
140+
* @param source The source config to use
141+
* @param startDelimiter Start delimiter.
142+
* @param endDelimiter End delimiter.
143+
* @return A processed string.
144+
*/
145+
public String resolve(final String text) {
146+
requireNonNull(text, "Text is required.");
147+
if (text.length() == 0) {
148+
return "";
149+
}
150+
151+
BiFunction<Integer, BiFunction<Integer, Integer, RuntimeException>, RuntimeException> err = (
152+
start, ex) -> {
153+
String snapshot = text.substring(0, start);
154+
int line = Splitter.on('\n').splitToList(snapshot).size();
155+
int column = start - snapshot.lastIndexOf('\n');
156+
return ex.apply(line, column);
157+
};
158+
159+
StringBuilder buffer = new StringBuilder();
160+
int offset = 0;
161+
int start = text.indexOf(startDelim);
162+
while (start >= 0) {
163+
int end = text.indexOf(endDelim, start + startDelim.length());
164+
if (end == -1) {
165+
throw err.apply(start, (line, column) -> new IllegalArgumentException(
166+
"found '" + startDelim + "' expecting '" + endDelim + "' at " + line + ":"
167+
+ column));
168+
}
169+
buffer.append(text.substring(offset, start));
170+
String key = text.substring(start + startDelim.length(), end);
171+
Object value;
172+
if (source.hasPath(key)) {
173+
value = source.getAnyRef(key);
174+
} else {
175+
if (ignoreMissing) {
176+
value = text.substring(start, end + endDelim.length());
177+
} else {
178+
throw err.apply(start, (line, column) -> new NoSuchElementException(
179+
"No configuration setting found for key '" + key + "' at " + line + ":" + column));
180+
}
181+
}
182+
buffer.append(value);
183+
offset = end + endDelim.length();
184+
start = text.indexOf(startDelim, offset);
185+
}
186+
if (buffer.length() == 0) {
187+
return text;
188+
}
189+
if (offset < text.length()) {
190+
buffer.append(text.substring(offset));
191+
}
192+
return buffer.toString();
193+
}
194+
}
195+
73196
/**
74197
* Utility class for generating {@link Key} for named services.
75198
*
@@ -258,99 +381,17 @@ default ServiceKey serviceKey() {
258381
* @return A processed string.
259382
*/
260383
default String resolve(final String text) {
261-
return resolve(text, config());
262-
}
263-
264-
/**
265-
* Returns a string with all substitutions (the <code>${foo.bar}</code> syntax,
266-
* see <a href="https://github.com/typesafehub/config/blob/master/HOCON.md">the
267-
* spec</a>) resolved. Substitutions are looked up using the {@link #config()} as the root object,
268-
* that is, a substitution <code>${foo.bar}</code> will be replaced with
269-
* the result of <code>getValue("foo.bar")</code>.
270-
*
271-
* @param text Text to process.
272-
* @param startDelimiter Start delimiter.
273-
* @param endDelimiter End delimiter.
274-
* @return A processed string.
275-
*/
276-
default String resolve(final String text, final String startDelimiter,
277-
final String endDelimiter) {
278-
return resolve(text, config(), startDelimiter, endDelimiter);
279-
}
280-
281-
/**
282-
* Returns a string with all substitutions (the <code>${foo.bar}</code> syntax,
283-
* see <a href="https://github.com/typesafehub/config/blob/master/HOCON.md">the
284-
* spec</a>) resolved. Substitutions are looked up using the <code>source</code> param as the
285-
* root object, that is, a substitution <code>${foo.bar}</code> will be replaced with
286-
* the result of <code>getValue("foo.bar")</code>.
287-
*
288-
* @param text Text to process.
289-
* @param source The source config to use.
290-
* @return A processed string.
291-
*/
292-
default String resolve(final String text, final Config source) {
293-
return resolve(text, source, "${", "}");
384+
return resolver().resolve(text);
294385
}
295386

296387
/**
297-
* Returns a string with all substitutions (the <code>${foo.bar}</code> syntax,
298-
* see <a href="https://github.com/typesafehub/config/blob/master/HOCON.md">the
299-
* spec</a>) resolved. Substitutions are looked up using the <code>source</code> param as the
300-
* root object, that is, a substitution <code>${foo.bar}</code> will be replaced with
301-
* the result of <code>getValue("foo.bar")</code>.
388+
* Creates a new environment {@link Resolver}.
302389
*
303-
* @param text Text to process.
304-
* @param source The source config to use
305-
* @param startDelimiter Start delimiter.
306-
* @param endDelimiter End delimiter.
307-
* @return A processed string.
390+
* @return
308391
*/
309-
default String resolve(final String text, final Config source,
310-
final String startDelimiter, final String endDelimiter) {
311-
requireNonNull(text, "Text is required.");
312-
requireNonNull(source, "Config source is required.");
313-
checkArgument(!Strings.isNullOrEmpty(startDelimiter), "Start delimiter is required.");
314-
checkArgument(!Strings.isNullOrEmpty(endDelimiter), "End delimiter is required.");
315-
if (text.length() == 0) {
316-
return "";
317-
}
318-
319-
BiFunction<Integer, BiFunction<Integer, Integer, RuntimeException>, RuntimeException> err = (
320-
start, ex) -> {
321-
String snapshot = text.substring(0, start);
322-
int line = Splitter.on('\n').splitToList(snapshot).size();
323-
int column = start - snapshot.lastIndexOf('\n');
324-
return ex.apply(line, column);
325-
};
326-
327-
StringBuilder buffer = new StringBuilder();
328-
int offset = 0;
329-
int start = text.indexOf(startDelimiter);
330-
while (start >= 0) {
331-
int end = text.indexOf(endDelimiter, start + startDelimiter.length());
332-
if (end == -1) {
333-
throw err.apply(start, (line, column) -> new IllegalArgumentException(
334-
"found '" + startDelimiter + "' expecting '" + endDelimiter + "' at " + line + ":"
335-
+ column));
336-
}
337-
buffer.append(text.substring(offset, start));
338-
String key = text.substring(start + startDelimiter.length(), end);
339-
if (!source.hasPath(key)) {
340-
throw err.apply(start, (line, column) -> new NoSuchElementException(
341-
"No configuration setting found for key '" + key + "' at " + line + ":" + column));
342-
}
343-
buffer.append(source.getAnyRef(key));
344-
offset = end + endDelimiter.length();
345-
start = text.indexOf(startDelimiter, offset);
346-
}
347-
if (buffer.length() == 0) {
348-
return text;
349-
}
350-
if (offset < text.length()) {
351-
buffer.append(text.substring(offset));
352-
}
353-
return buffer.toString();
392+
default Resolver resolver() {
393+
return new Resolver()
394+
.source(config());
354395
}
355396

356397
/**

jooby/src/test/java/org/jooby/EnvTest.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import org.junit.Test;
1515

16+
import com.google.common.collect.ImmutableMap;
1617
import com.typesafe.config.Config;
1718
import com.typesafe.config.ConfigFactory;
1819
import com.typesafe.config.ConfigValueFactory;
@@ -47,8 +48,8 @@ public void altplaceholder() {
4748
Env env = Env.DEFAULT.build(ConfigFactory.empty()
4849
.withValue("var", ConfigValueFactory.fromAnyRef("foo.bar")));
4950

50-
assertEquals("foo.bar", env.resolve("{{var}}", "{{", "}}"));
51-
assertEquals("foo.bar", env.resolve("<%var%>", "<%", "%>"));
51+
assertEquals("foo.bar", env.resolver().delimiters("{{", "}}").resolve("{{var}}"));
52+
assertEquals("foo.bar", env.resolver().delimiters("<%", "%>").resolve("<%var%>"));
5253
}
5354

5455
@Test
@@ -78,6 +79,25 @@ public void resolveMore() {
7879
assertEquals("foo.bar - foo.bar", env.resolve("${var} - ${var}"));
7980
}
8081

82+
@Test
83+
public void resolveMap() {
84+
Config config = ConfigFactory.empty();
85+
86+
Env env = Env.DEFAULT.build(config);
87+
assertEquals("foo.bar - foo.bar", env.resolver().source(ImmutableMap.of("var", "foo.bar"))
88+
.resolve("${var} - ${var}"));
89+
}
90+
91+
@Test
92+
public void resolveIgnoreMissing() {
93+
Config config = ConfigFactory.empty();
94+
95+
Env env = Env.DEFAULT.build(config);
96+
assertEquals("${var} - ${var}", env.resolver().ignoreMissing().resolve("${var} - ${var}"));
97+
98+
assertEquals(" - ${foo.var} -", env.resolver().ignoreMissing().resolve(" - ${foo.var} -"));
99+
}
100+
81101
@Test
82102
public void novars() {
83103
Config config = ConfigFactory.empty()
@@ -156,18 +176,6 @@ public void nullText() {
156176
env.resolve(null);
157177
}
158178

159-
@Test(expected = IllegalArgumentException.class)
160-
public void emptyStartDelim() {
161-
Env env = Env.DEFAULT.build(ConfigFactory.empty());
162-
env.resolve("{{var}}", "", "}");
163-
}
164-
165-
@Test(expected = IllegalArgumentException.class)
166-
public void emptyEndDelim() {
167-
Env env = Env.DEFAULT.build(ConfigFactory.empty());
168-
env.resolve("{{var}}", "${", "");
169-
}
170-
171179
@Test
172180
public void unclosedDelimiterWithSpace() {
173181
Env env = Env.DEFAULT.build(ConfigFactory.empty());

0 commit comments

Comments
 (0)