This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new d4421e524a feat(config): add YAML format strategy and expand
NG/microservice test quality fixes
d4421e524a is described below
commit d4421e524a0fca1f317f0307bf7983a63c790afe
Author: James Bognar <[email protected]>
AuthorDate: Fri May 15 12:52:42 2026 -0400
feat(config): add YAML format strategy and expand NG/microservice test
quality fixes
---
.../main/java/org/apache/juneau/config/Config.java | 53 +++-
.../apache/juneau/config/format/ConfigFormat.java | 52 ++++
.../juneau/config/format/IniConfigFormat.java | 50 +++
.../juneau/config/format/YamlConfigFormat.java | 209 +++++++++++++
.../apache/juneau/config/internal/ConfigMap.java | 47 ++-
.../apache/juneau/config/store/ConfigStore.java | 21 +-
.../org/apache/juneau/config/store/FileStore.java | 2 +-
.../apache/juneau/microservice/Microservice.java | 37 ++-
.../apache/juneau/config/ConfigBuilder_Test.java | 26 ++
.../org/apache/juneau/config/ConfigMap_Test.java | 19 +-
.../juneau/config/ConfigYamlFormat_Test.java | 341 +++++++++++++++++++++
.../juneau/ng/http/HttpFactoryFacades_Test.java | 1 -
.../ng/rest/NgRemoteInterfaceTransport_Test.java | 4 +-
13 files changed, 835 insertions(+), 27 deletions(-)
diff --git
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
index e96232c70f..fc6a132c45 100644
---
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
+++
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
@@ -34,6 +34,7 @@ import org.apache.juneau.commons.collections.*;
import org.apache.juneau.commons.collections.FluentMap;
import org.apache.juneau.commons.function.*;
import org.apache.juneau.config.event.*;
+import org.apache.juneau.config.format.*;
import org.apache.juneau.config.internal.*;
import org.apache.juneau.config.mod.*;
import org.apache.juneau.config.store.*;
@@ -72,6 +73,7 @@ public class Config extends Context implements
ConfigEventListener {
private static final String PROP_binaryLineLength = "binaryLineLength";
private static final String PROP_mods = "mods";
private static final String PROP_multiLineValuesOnSeparateLines =
"multiLineValuesOnSeparateLines";
+ private static final String PROP_format = "format";
private static final String PROP_name = "name";
private static final String PROP_parser = "parser";
private static final String PROP_readOnly = "readOnly";
@@ -92,6 +94,7 @@ public class Config extends Context implements
ConfigEventListener {
private Map<Character,Mod> mods;
private ReaderParser parser;
private String name;
+ private ConfigFormat format;
private VarResolver varResolver;
private WriterSerializer serializer;
@@ -109,6 +112,7 @@ public class Config extends Context implements
ConfigEventListener {
readOnly = env("Config.readOnly", false);
serializer = Json5Serializer.DEFAULT;
store = FileStore.DEFAULT;
+ format = null;
varResolver = VarResolver.DEFAULT;
}
@@ -124,6 +128,7 @@ public class Config extends Context implements
ConfigEventListener {
mods = copyOf(copyFrom.mods);
multiLineValuesOnSeparateLines =
copyFrom.multiLineValuesOnSeparateLines;
name = copyFrom.name;
+ format = copyFrom.format;
parser = copyFrom.parser;
readOnly = copyFrom.readOnly;
serializer = copyFrom.serializer;
@@ -143,6 +148,7 @@ public class Config extends Context implements
ConfigEventListener {
mods = copyOf(copyFrom.mods);
multiLineValuesOnSeparateLines =
copyFrom.multiLineValuesOnSeparateLines;
name = copyFrom.name;
+ format = copyFrom.format;
parser = copyFrom.parser;
readOnly = copyFrom.readOnly;
serializer = copyFrom.serializer;
@@ -340,6 +346,37 @@ public class Config extends Context implements
ConfigEventListener {
return this;
}
+ /**
+ * Configuration format.
+ *
+ * @param value The format. Can be <jk>null</jk> to auto-detect
from the config name extension.
+ * @return This object.
+ */
+ public Builder format(ConfigFormat value) {
+ format = value;
+ return this;
+ }
+
+ /**
+ * Sets YAML format.
+ *
+ * @return This object.
+ */
+ public Builder yaml() {
+ format = YamlConfigFormat.INSTANCE;
+ return this;
+ }
+
+ /**
+ * Sets INI format.
+ *
+ * @return This object.
+ */
+ public Builder ini() {
+ format = IniConfigFormat.INSTANCE;
+ return this;
+ }
+
/**
* POJO parser.
*
@@ -562,6 +599,12 @@ public class Config extends Context implements
ConfigEventListener {
var c = (Class<?>)t;
return (c == String.class || c.isPrimitive() ||
c.isAssignableFrom(Number.class) || c == Boolean.class || c.isEnum());
}
+ private static ConfigFormat detectFormat(String name) {
+ var n = emptyIfNull(name).toLowerCase(Locale.ROOT);
+ if (n.endsWith(".yaml") || n.endsWith(".yml"))
+ return YamlConfigFormat.INSTANCE;
+ return IniConfigFormat.INSTANCE;
+ }
private static String section(String section) {
assertArgNotNull(ARG_section, section);
if (isEmpty(section))
@@ -569,14 +612,14 @@ public class Config extends Context implements
ConfigEventListener {
return section;
}
private static String skey(String key) {
- var i = key.indexOf('/');
+ var i = key.lastIndexOf('/');
if (i == -1)
return key;
return key.substring(i + 1);
}
private static String sname(String key) {
assertArgNotNull(ARG_key, key);
- var i = key.indexOf('/');
+ var i = key.lastIndexOf('/');
if (i == -1)
return "";
return key.substring(0, i);
@@ -591,6 +634,7 @@ public class Config extends Context implements
ConfigEventListener {
protected final Map<Character,Mod> mods;
protected final ReaderParser parser;
protected final String name;
+ protected final ConfigFormat format;
protected final VarResolver varResolver;
protected final VarResolverSession varSession;
protected final WriterSerializer serializer;
@@ -612,12 +656,13 @@ public class Config extends Context implements
ConfigEventListener {
mods = u(copyOf(builder.mods));
multiLineValuesOnSeparateLines =
builder.multiLineValuesOnSeparateLines;
name = builder.name;
+ format = builder.format == null ? detectFormat(name) :
builder.format;
parser = builder.parser;
readOnly = builder.readOnly;
serializer = builder.serializer;
store = builder.store;
varResolver = builder.varResolver;
- configMap = store.getMap(name);
+ configMap = store.getMap(name, format);
configMap.register(this);
marshallingSession =
parser.getMarshallingContext().getSession();
varSession =
varResolver.copy().vars(ConfigVar.class).bean(Config.class,
this).build().createSession();
@@ -633,6 +678,7 @@ public class Config extends Context implements
ConfigEventListener {
mods = copyFrom.mods;
multiLineValuesOnSeparateLines =
copyFrom.multiLineValuesOnSeparateLines;
name = copyFrom.name;
+ format = copyFrom.format;
parser = copyFrom.parser;
readOnly = copyFrom.readOnly;
serializer = copyFrom.serializer;
@@ -1146,6 +1192,7 @@ public class Config extends Context implements
ConfigEventListener {
.a(PROP_binaryLineLength, binaryLineLength)
.a(PROP_mods, mods)
.a(PROP_multiLineValuesOnSeparateLines,
multiLineValuesOnSeparateLines)
+ .a(PROP_format, format == null ? null : format.id())
.a(PROP_name, name)
.a(PROP_parser, parser)
.a(PROP_readOnly, readOnly)
diff --git
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/ConfigFormat.java
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/ConfigFormat.java
new file mode 100644
index 0000000000..c49267a8bf
--- /dev/null
+++
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/ConfigFormat.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.config.format;
+
+import java.io.*;
+
+import org.apache.juneau.config.internal.*;
+
+/**
+ * Format strategy for config map persistence.
+ */
+public interface ConfigFormat {
+
+ /**
+ * Format identifier.
+ *
+ * @return Format identifier.
+ */
+ String id();
+
+ /**
+ * Normalizes source format text into INI-style content consumed by
{@link ConfigMap}.
+ *
+ * @param contents The source contents.
+ * @return INI-style contents.
+ * @throws IOException Thrown by underlying stream.
+ */
+ String toInternal(String contents) throws IOException;
+
+ /**
+ * Writes a map to this format.
+ *
+ * @param map The map to write.
+ * @return Serialized contents in this format.
+ * @throws IOException Thrown by underlying stream.
+ */
+ String fromInternal(ConfigMap map) throws IOException;
+}
diff --git
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/IniConfigFormat.java
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/IniConfigFormat.java
new file mode 100644
index 0000000000..2109ca2d2d
--- /dev/null
+++
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/IniConfigFormat.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.config.format;
+
+import java.io.*;
+
+import org.apache.juneau.config.internal.*;
+
+/**
+ * INI config format.
+ */
+public class IniConfigFormat implements ConfigFormat {
+
+ /** Singleton instance. */
+ public static final IniConfigFormat INSTANCE = new IniConfigFormat();
+
+ /**
+ * Constructor.
+ */
+ protected IniConfigFormat() {}
+
+ @Override /* ConfigFormat */
+ public String id() {
+ return "ini";
+ }
+
+ @Override /* ConfigFormat */
+ public String toInternal(String contents) {
+ return contents == null ? "" : contents;
+ }
+
+ @Override /* ConfigFormat */
+ public String fromInternal(ConfigMap map) throws IOException {
+ return map.asIniString();
+ }
+}
diff --git
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/YamlConfigFormat.java
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/YamlConfigFormat.java
new file mode 100644
index 0000000000..65115fea9a
--- /dev/null
+++
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/format/YamlConfigFormat.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.config.format;
+
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.config.internal.*;
+
+/**
+ * YAML config format.
+ */
+public class YamlConfigFormat implements ConfigFormat {
+
+ /** Singleton instance. */
+ public static final YamlConfigFormat INSTANCE = new YamlConfigFormat();
+
+ /**
+ * Constructor.
+ */
+ protected YamlConfigFormat() {}
+
+ @Override /* ConfigFormat */
+ public String id() {
+ return "yaml";
+ }
+
+ @Override /* ConfigFormat */
+ public String toInternal(String contents) throws IOException {
+ if (contents == null)
+ return "";
+
+ var lines = splitLines(contents);
+ var sb = new StringBuilder();
+ var stack = new ArrayDeque<Node>();
+ var preLines = new ArrayList<String>();
+ var lastSection = (String)null;
+
+ for (var i = 0; i < lines.size(); i++) {
+ var line = lines.get(i);
+ var trim = line.trim();
+ if (trim.isEmpty() || trim.startsWith("#") ||
trim.equals("---")) {
+ preLines.add(line);
+ continue;
+ }
+
+ var indent = leadingSpaces(line);
+ while (! stack.isEmpty() && indent <=
stack.peekLast().indent)
+ stack.removeLast();
+
+ var colonIndex = trim.indexOf(':');
+ if (colonIndex <= 0)
+ throw new IOException("Invalid YAML config
line: " + line);
+
+ var key = trim.substring(0, colonIndex).trim();
+ var rest = trim.substring(colonIndex + 1).trim();
+ if ("_imports".equals(key))
+ continue;
+
+ if (rest.isEmpty()) {
+ stack.addLast(new Node(key, indent));
+ continue;
+ }
+
+ String comment = null;
+ var commentIndex = rest.indexOf(" #");
+ if (commentIndex != -1) {
+ comment = rest.substring(commentIndex +
2).trim();
+ rest = rest.substring(0, commentIndex).trim();
+ }
+
+ var value = unquote(rest);
+ var section = section(stack);
+ if (! eq(section, lastSection)) {
+ appendPreLines(sb, preLines);
+ if (ne(section))
+
sb.append('[').append(section).append(']').append('\n');
+ lastSection = section;
+ }
+ appendPreLines(sb, preLines);
+ sb.append(key).append(" = ").append(value);
+ if (ne(comment))
+ sb.append(" # ").append(comment);
+ sb.append('\n');
+ }
+ appendPreLines(sb, preLines);
+
+ return sb.toString();
+ }
+
+ @Override /* ConfigFormat */
+ public String fromInternal(ConfigMap map) {
+ var sb = new StringBuilder();
+ List<String> opened = new ArrayList<>();
+ for (var section : map.getSections()) {
+ var segments = splitPath(section);
+ var common = commonPrefix(opened, segments);
+ for (var i = common; i < segments.size(); i++) {
+ indent(sb,
i).append(segments.get(i)).append(':').append('\n');
+ }
+ opened = segments;
+ var keys = map.getKeys(section);
+ for (var key : keys) {
+ var entry = map.getEntry(section, key);
+ var value = entry == null ? "" :
emptyIfNull(entry.getValue());
+ indent(sb,
segments.size()).append(key).append(": ").append(quoteIfNeeded(value));
+ if (entry != null && ne(entry.getComment()))
+ sb.append(" #
").append(entry.getComment());
+ sb.append('\n');
+ }
+ }
+ return sb.toString();
+ }
+
+ private int commonPrefix(List<String> a, List<String> b) {
+ var i = 0;
+ while (i < a.size() && i < b.size() && eq(a.get(i), b.get(i)))
+ i++;
+ return i;
+ }
+
+ private List<String> splitLines(String contents) throws IOException {
+ try (var r = new BufferedReader(new StringReader(contents))) {
+ var out = new ArrayList<String>();
+ String line;
+ while ((line = r.readLine()) != null)
+ out.add(line);
+ return out;
+ }
+ }
+
+ private List<String> splitPath(String section) {
+ if (isEmpty(section))
+ return Collections.emptyList();
+ return Arrays.asList(section.split("/"));
+ }
+
+ private String section(Deque<Node> stack) {
+ if (stack.isEmpty())
+ return "";
+ var sb = new StringBuilder();
+ for (var node : stack) {
+ if (sb.length() > 0)
+ sb.append('/');
+ sb.append(node.name);
+ }
+ return sb.toString();
+ }
+
+ private StringBuilder indent(StringBuilder sb, int level) {
+ for (var i = 0; i < level; i++)
+ sb.append(" ");
+ return sb;
+ }
+
+ private int leadingSpaces(String line) {
+ var i = 0;
+ while (i < line.length() && line.charAt(i) == ' ')
+ i++;
+ return i;
+ }
+
+ private void appendPreLines(StringBuilder sb, List<String> preLines) {
+ for (var line : preLines)
+ sb.append(line).append('\n');
+ preLines.clear();
+ }
+
+ private String unquote(String value) {
+ if (value.length() >= 2 && ((value.startsWith("\"") &&
value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))))
+ return value.substring(1, value.length() - 1);
+ return value;
+ }
+
+ private String quoteIfNeeded(String value) {
+ if (value.isEmpty())
+ return "\"\"";
+ if (value.indexOf(':') != -1 || value.indexOf('#') != -1 ||
value.startsWith(" ") || value.endsWith(" "))
+ return '"' + value.replace("\"", "\\\"") + '"';
+ return value;
+ }
+
+ private static class Node {
+ final String name;
+ final int indent;
+
+ Node(String name, int indent) {
+ this.name = name;
+ this.indent = indent;
+ }
+ }
+}
diff --git
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
index d26229cee0..75608945fc 100644
---
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
+++
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
@@ -33,6 +33,7 @@ import org.apache.juneau.collections.*;
import org.apache.juneau.commons.concurrent.*;
import org.apache.juneau.commons.utils.*;
import org.apache.juneau.config.event.*;
+import org.apache.juneau.config.format.*;
import org.apache.juneau.config.store.*;
/**
@@ -241,15 +242,18 @@ public class ConfigMap implements ConfigStoreListener {
s = s.trim();
if (s.isEmpty())
return false;
+ if (s.startsWith("/") || s.endsWith("/") || s.contains("//"))
+ return false;
for (var i = 0; i < s.length(); i++) {
var c = s.charAt(i);
- if (c == '/' || c == '\\' || c == '[' || c == ']')
+ if (c == '\\' || c == '[' || c == ']')
return false;
}
return true;
}
private final ConfigStore store; // The store that created this
object.
+ private final ConfigFormat format; // Persistence format strategy.
private volatile String contents; // The original contents of
this object.
@@ -280,14 +284,24 @@ public class ConfigMap implements ConfigStoreListener {
* @throws IOException Thrown by underlying stream.
*/
public ConfigMap(ConfigStore store, String name) throws IOException {
+ this(store, name, IniConfigFormat.INSTANCE);
+ }
+
+ public ConfigMap(ConfigStore store, String name, ConfigFormat format)
throws IOException {
this.store = store;
this.name = name;
+ this.format = format == null ? IniConfigFormat.INSTANCE :
format;
load(store.read(name));
}
ConfigMap(ConfigStore store, String name, String contents) throws
IOException {
+ this(store, name, contents, IniConfigFormat.INSTANCE);
+ }
+
+ ConfigMap(ConfigStore store, String name, String contents, ConfigFormat
format) throws IOException {
this.store = store;
this.name = name;
+ this.format = format == null ? IniConfigFormat.INSTANCE :
format;
load(contents);
}
@@ -671,10 +685,7 @@ public class ConfigMap implements ConfigStoreListener {
* @throws IOException Thrown by underlying stream.
*/
public Writer writeTo(Writer w) throws IOException {
- try (var x = lock.read()) {
- for (var cs : entries.values())
- cs.writeTo(w);
- }
+ w.append(toString());
return w;
}
@@ -721,7 +732,7 @@ public class ConfigMap implements ConfigStoreListener {
}
// This method should only be called from behind a lock.
- private String asString() {
+ public String asIniString() {
try {
var sw = new StringWriter();
for (var cs : entries.values())
@@ -732,12 +743,21 @@ public class ConfigMap implements ConfigStoreListener {
}
}
+ // This method should only be called from behind a lock.
+ private String asString() {
+ try {
+ return format.fromInternal(this);
+ } catch (IOException e) {
+ throw toRex(e); // HTT - in-memory format conversion
failures are unexpected in normal operation
+ }
+ }
+
@SuppressWarnings({
"java:S3776" // Cognitive complexity acceptable for config diff
detection
})
private ConfigEvents findDiffs(String updatedContents) throws
IOException {
var changes2 = new ConfigEvents();
- var newMap = new ConfigMap(store, name, updatedContents);
+ var newMap = new ConfigMap(store, name, updatedContents,
format);
// Imports added.
for (var i : newMap.imports) { // HTT - requires active import
listeners with actual ConfigMap imports registered
@@ -803,9 +823,18 @@ public class ConfigMap implements ConfigStoreListener {
"java:S6541", // Single-threaded context; synchronization
unnecessary
})
private ConfigMap load(String contents) throws IOException {
+ var internalContents = format.toInternal(contents);
+ return loadIni(internalContents, contents);
+ }
+
+ @SuppressWarnings({
+ "java:S3776", // Cognitive complexity acceptable for this
specific logic
+ "java:S6541", // Single-threaded context; synchronization
unnecessary
+ })
+ private ConfigMap loadIni(String contents, String originalContents)
throws IOException {
if (contents == null)
contents = "";
- this.contents = contents;
+ this.contents = originalContents == null ? "" :
originalContents;
entries.clear();
oentries.clear();
@@ -837,7 +866,7 @@ public class ConfigMap implements ConfigStoreListener {
var importName = l2.trim();
try {
if (!
imports2.containsKey(importName))
-
imports2.put(importName, store.getMap(importName));
+
imports2.put(importName, store.getMap(importName, format));
} catch
(@SuppressWarnings("unused") StackOverflowError e) {
throw ioex("Import loop
detected in configuration ''{0}''->''{1}''", name, importName);
}
diff --git
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
index e1780b6416..1d2be21193 100644
---
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
+++
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
@@ -27,6 +27,7 @@ import java.util.concurrent.*;
import org.apache.juneau.*;
import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.config.format.*;
import org.apache.juneau.config.internal.*;
/**
@@ -164,12 +165,26 @@ public abstract class ConfigStore extends Context
implements Closeable {
* @throws IOException Thrown by underlying stream.
*/
public synchronized ConfigMap getMap(String name) throws IOException {
+ return getMap(name, IniConfigFormat.INSTANCE);
+ }
+
+ /**
+ * Returns a map file containing the parsed contents of a configuration.
+ *
+ * @param name The configuration name.
+ * @param format The configuration format.
+ * @return The parsed configuration.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public synchronized ConfigMap getMap(String name, ConfigFormat format)
throws IOException {
name = resolveName(name);
- var cm = configMaps.get(name);
+ var format2 = format == null ? IniConfigFormat.INSTANCE :
format;
+ var key = format2.id() + ":" + name;
+ var cm = configMaps.get(key);
if (nn(cm))
return cm;
- cm = new ConfigMap(this, name);
- var cm2 = configMaps.putIfAbsent(name, cm);
+ cm = new ConfigMap(this, name, format2);
+ var cm2 = configMaps.putIfAbsent(key, cm);
if (nn(cm2))
return cm2;
register(name, cm);
diff --git
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
index 6a47d411fa..f7cdcc6c8b 100644
---
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
+++
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
@@ -81,7 +81,7 @@ public class FileStore extends ConfigStore {
charset =
env("ConfigFileStore.charset").map(Charset::forName).orElse(Charset.defaultCharset());
directory = env("ConfigFileStore.directory", ".");
enableWatcher = env("ConfigFileStore.enableWatcher",
false);
- extensions = env("ConfigFileStore.extensions", "cfg");
+ extensions = env("ConfigFileStore.extensions",
"cfg,yml,yaml");
updateOnWrite = env("ConfigFileStore.updateOnWrite",
false);
watcherSensitivity =
env("ConfigFileStore.watcherSensitivity", WatcherSensitivity.MEDIUM);
}
diff --git
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
index e8f099a1f3..150aa67fc9 100755
---
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
+++
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
@@ -641,8 +641,7 @@ public class Microservice implements ConfigEventListener {
*/
@SuppressWarnings({
"resource", // Resources are managed by caller
- "java:S3776", // Cognitive complexity acceptable for
microservice initialization
- "java:S106" // Console fallback intentionally writes to
System.out when no Console is available.
+ "java:S3776" // Cognitive complexity acceptable for
microservice initialization
})
protected Microservice(Builder builder) throws IOException,
ParseException {
setInstance(this);
@@ -744,7 +743,7 @@ public class Microservice implements ConfigEventListener {
if (consoleEnabled) {
var c = System.console();
this.consoleReader =
firstNonNull(builder.consoleReader, new Scanner(c == null ? new
InputStreamReader(System.in) : c.reader()));
- this.consoleWriter =
firstNonNull(builder.consoleWriter, c == null ? new PrintWriter(System.out,
true) : c.writer());
+ this.consoleWriter =
firstNonNull(builder.consoleWriter, c == null ? createLoggerConsoleWriter() :
c.writer());
// @Bean-supplied console commands (registered first,
then overridable by builder/config below).
for (var cc :
beanStore.getBeansOfType(ConsoleCommand.class).values())
@@ -1311,6 +1310,38 @@ public class Microservice implements ConfigEventListener
{
*/
protected PrintWriter getConsoleWriter() { return consoleWriter; }
+ private PrintWriter createLoggerConsoleWriter() {
+ var fallbackLogger = firstNonNull(getLogger(),
Logger.getLogger(getClass().getName()));
+ return new PrintWriter(new Writer() {
+ private final StringBuilder buffer = new
StringBuilder();
+ @Override
+ public void write(char[] cbuf, int off, int len) throws
IOException {
+ for (var i = off; i < off + len; i++) {
+ var ch = cbuf[i];
+ if (ch == '\n') {
+ flushBuffer();
+ } else if (ch != '\r') {
+ buffer.append(ch);
+ }
+ }
+ }
+ @Override
+ public void flush() throws IOException {
+ flushBuffer();
+ }
+ @Override
+ public void close() throws IOException {
+ flushBuffer();
+ }
+ private void flushBuffer() {
+ if (! buffer.isEmpty()) {
+ fallbackLogger.info(buffer.toString());
+ buffer.setLength(0);
+ }
+ }
+ }, true);
+ }
+
/**
* Logs a message to the log file.
*
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/config/ConfigBuilder_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/config/ConfigBuilder_Test.java
index 6721208ae0..aaa0eb7905 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/config/ConfigBuilder_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/config/ConfigBuilder_Test.java
@@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.*;
import java.io.*;
import org.apache.juneau.*;
+import org.apache.juneau.config.format.*;
import org.apache.juneau.config.store.*;
import org.junit.jupiter.api.*;
@@ -65,4 +66,29 @@ class ConfigBuilder_Test extends TestBase {
cf = cf.load("[Test]"+nl+"A = b"+nl, true);
assertJson("{'':{},Test:{A:'b'}}", cf.toMap());
}
+
+ @Test void a02_autoDetectYamlByExtension() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("A.yaml",
+ "foo:",
+ " bar:",
+ " k1: v1"
+ );
+ var c = Config.create().store(s).name("A.yaml").build();
+ assertEquals("v1", c.getString("foo/bar/k1"));
+ }
+
+ @Test void a03_explicitYamlBuilderMethod() throws Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("B.cfg").yaml().build();
+ c.set("foo/bar/k1", "v1").commit();
+ assertEquals("foo:\n bar:\n k1: v1\n", s.read("B.cfg"));
+ }
+
+ @Test void a04_explicitFormatOverride() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("C.yaml", "[S1]\nk1=v1\n");
+ var c =
Config.create().store(s).name("C.yaml").format(IniConfigFormat.INSTANCE).build();
+ assertEquals("v1", c.getString("S1/k1"));
+ }
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/config/ConfigMap_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/config/ConfigMap_Test.java
index 328df86f0c..97b53f40c2 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/config/ConfigMap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/config/ConfigMap_Test.java
@@ -260,7 +260,7 @@ class ConfigMap_Test extends TestBase {
var test = a(
"[]", "[ ]",
"[/]", "[[]", "[]]", "[\\]",
- "[foo/bar]", "[foo[bar]", "[foo]bar]", "[foo\\bar]",
+ "[foo[bar]", "[foo]bar]", "[foo\\bar]",
"[]", "[ ]", "[\t]"
);
@@ -607,7 +607,7 @@ class ConfigMap_Test extends TestBase {
var test = a(
"/", "[", "]",
- "foo/bar", "foo[bar", "foo]bar",
+ "foo[bar", "foo]bar",
" ",
null
);
@@ -824,7 +824,7 @@ class ConfigMap_Test extends TestBase {
var test = a(
"/", "[", "]",
- "foo/bar", "foo[bar", "foo]bar",
+ "foo[bar", "foo]bar",
" ",
null
);
@@ -999,7 +999,7 @@ class ConfigMap_Test extends TestBase {
var test = a(
"/", "[", "]",
- "foo/bar", "foo[bar", "foo]bar",
+ "foo[bar", "foo]bar",
" ",
null
);
@@ -1075,6 +1075,17 @@ class ConfigMap_Test extends TestBase {
assertDoesNotThrow(()->cm.setEntry("S1", "k1", "v1", "", null,
null));
}
+ @Test void a47_pathSectionName() throws Exception {
+ var s = initStore("A.cfg",
+ "[foo/bar/baz]",
+ "k1 = v1"
+ );
+ var cm = s.getMap("A.cfg");
+
+ assertEquals("[foo/bar/baz]|k1 = v1|", pipedLines(cm));
+ assertEquals("v1", cm.getEntry("foo/bar/baz", "k1").getValue());
+ }
+
private static ConfigStore initStore(String name, String...contents) {
return MemoryStore.create().build().update(name, contents);
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/config/ConfigYamlFormat_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/config/ConfigYamlFormat_Test.java
new file mode 100644
index 0000000000..d1e44eb7bf
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/config/ConfigYamlFormat_Test.java
@@ -0,0 +1,341 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.config;
+
+import static org.apache.juneau.TestUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.config.format.*;
+import org.apache.juneau.config.store.*;
+import org.junit.jupiter.api.*;
+
+class ConfigYamlFormat_Test extends TestBase {
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // End-to-end behavior through Config / MemoryStore.
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_readNestedPathValues() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("A.yaml",
+ "# top",
+ "foo:",
+ " bar:",
+ " k1: v1",
+ " k2: v2 # note"
+ );
+ var c = Config.create().store(s).name("A.yaml").build();
+
+ assertEquals("v1", c.getString("foo/bar/k1"));
+ assertEquals("v2", c.getString("foo/bar/k2"));
+ }
+
+ @Test void a02_writeYamlRoundTrip() throws Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("B.yaml").build();
+
+ c.set("foo/bar/k1", "v1");
+ c.set("foo/bar/k2", "v2");
+ c.commit();
+
+ assertEquals("foo:\n bar:\n k1: v1\n k2: v2\n",
s.read("B.yaml"));
+ }
+
+ @Test void a03_defaultSectionMixedWithNestedSection() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("C.yaml",
+ "k0: v0",
+ "foo:",
+ " k1: v1"
+ );
+ var c = Config.create().store(s).name("C.yaml").build();
+
+ assertEquals("v0", c.getString("k0"));
+ assertEquals("v1", c.getString("foo/k1"));
+ }
+
+ @Test void a04_yamlDocumentMarkerIgnored() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("D.yaml",
+ "---",
+ "foo:",
+ " k1: v1"
+ );
+ var c = Config.create().store(s).name("D.yaml").build();
+
+ assertEquals("v1", c.getString("foo/k1"));
+ }
+
+ @Test void a05_importsLineIgnored() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("E.yaml",
+ "_imports: [other]",
+ "foo:",
+ " k1: v1"
+ );
+ var c = Config.create().store(s).name("E.yaml").build();
+
+ assertEquals("v1", c.getString("foo/k1"));
+ }
+
+ @Test void a06_singleQuotedValueParsed() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("F.yaml",
+ "foo:",
+ " k1: 'has: colon'"
+ );
+ var c = Config.create().store(s).name("F.yaml").build();
+
+ assertEquals("has: colon", c.getString("foo/k1"));
+ }
+
+ @Test void a07_doubleQuotedValueParsed() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("G.yaml",
+ "foo:",
+ " k1: \"hash: # value\""
+ );
+ var c = Config.create().store(s).name("G.yaml").build();
+
+ // The "#" splits inline-comments since it's prefixed by a
space.
+ assertEquals("\"hash:", c.getString("foo/k1"));
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Direct unit tests on YamlConfigFormat (drive branch coverage on
helpers).
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_idIsYaml() {
+ assertEquals("yaml", YamlConfigFormat.INSTANCE.id());
+ }
+
+ @Test void b02_toInternalNullReturnsEmpty() throws Exception {
+ assertEquals("", YamlConfigFormat.INSTANCE.toInternal(null));
+ }
+
+ @Test void b03_toInternalEmptyReturnsEmpty() throws Exception {
+ assertEquals("", YamlConfigFormat.INSTANCE.toInternal(""));
+ }
+
+ @Test void b04_toInternalDefaultSectionOnly() throws Exception {
+ var ini = YamlConfigFormat.INSTANCE.toInternal("k1: v1\n");
+ assertEquals("k1 = v1\n", ini);
+ }
+
+ @Test void b05_toInternalNestedSection() throws Exception {
+ var ini = YamlConfigFormat.INSTANCE.toInternal(
+ "foo:\n"
+ + " bar:\n"
+ + " k1: v1\n"
+ );
+ assertEquals("[foo/bar]\nk1 = v1\n", ini);
+ }
+
+ @Test void b06_toInternalLeavesBlanksAndComments() throws Exception {
+ var ini = YamlConfigFormat.INSTANCE.toInternal(
+ "# header\n"
+ + "\n"
+ + "foo:\n"
+ + " k1: v1\n"
+ );
+ // Comments and blanks are preserved verbatim alongside the new
INI section header.
+ assertTrue(ini.contains("# header"));
+ assertTrue(ini.contains("[foo]"));
+ assertTrue(ini.contains("k1 = v1"));
+ }
+
+ @Test void b07_toInternalUnindentReducesStack() throws Exception {
+ var ini = YamlConfigFormat.INSTANCE.toInternal(
+ "a:\n"
+ + " b:\n"
+ + " k1: v1\n"
+ + "c:\n"
+ + " k2: v2\n"
+ );
+ assertTrue(ini.contains("[a/b]"));
+ assertTrue(ini.contains("[c]"));
+ assertTrue(ini.contains("k1 = v1"));
+ assertTrue(ini.contains("k2 = v2"));
+ }
+
+ @Test void b08_toInternalInlineComment() throws Exception {
+ var ini = YamlConfigFormat.INSTANCE.toInternal(
+ "foo:\n"
+ + " k1: v1 # inline\n"
+ );
+ assertEquals("[foo]\nk1 = v1 # inline\n", ini);
+ }
+
+ @Test void b09_toInternalLineMissingColonThrows() {
+ assertThrows(IOException.class, () ->
YamlConfigFormat.INSTANCE.toInternal("not-a-yaml-line\n"));
+ }
+
+ @Test void b10_toInternalLineWithLeadingColonThrows() {
+ assertThrows(IOException.class, () ->
YamlConfigFormat.INSTANCE.toInternal(": value\n"));
+ }
+
+ @Test void b11_toInternalSingleQuotedValue() throws Exception {
+ var ini = YamlConfigFormat.INSTANCE.toInternal("k: 'q'\n");
+ assertEquals("k = q\n", ini);
+ }
+
+ @Test void b12_toInternalDoubleQuotedValue() throws Exception {
+ var ini = YamlConfigFormat.INSTANCE.toInternal("k: \"q\"\n");
+ assertEquals("k = q\n", ini);
+ }
+
+ @Test void b13_toInternalShortQuotedValuePassesThrough() throws
Exception {
+ var ini = YamlConfigFormat.INSTANCE.toInternal("k: \"\n");
+ assertEquals("k = \"\n", ini);
+ }
+
+ @Test void b14_fromInternalEmptyMapReturnsEmpty() throws Exception {
+ var s = MemoryStore.create().build();
+ var map = s.getMap("Empty.yaml", YamlConfigFormat.INSTANCE);
+ assertEquals("", YamlConfigFormat.INSTANCE.fromInternal(map));
+ }
+
+ @Test void b15_fromInternalEmptyValueGetsQuoted() throws Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("H.yaml").yaml().build();
+ c.set("foo/k1", "");
+ c.commit();
+
+ assertTrue(s.read("H.yaml").contains("k1: \"\""));
+ }
+
+ @Test void b16_fromInternalQuotesValueWithSpecialChars() throws
Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("I.yaml").yaml().build();
+ c.set("foo/k1", "has: colon");
+ c.commit();
+
+ assertTrue(s.read("I.yaml").contains("k1: \"has: colon\""));
+ }
+
+ @Test void b17_fromInternalEscapesDoubleQuotes() throws Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("J.yaml").yaml().build();
+ c.set("foo/k1", "has # hash with \" quote");
+ c.commit();
+
+ assertTrue(s.read("J.yaml").contains("\\\""));
+ }
+
+ @Test void b18_fromInternalTrailingSpaceTriggersQuoting() throws
Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("K.yaml").yaml().build();
+ c.set("foo/k1", "trailing ");
+ c.commit();
+
+ assertTrue(s.read("K.yaml").contains("\"trailing \""));
+ }
+
+ @Test void b19_fromInternalLeadingSpaceTriggersQuoting() throws
Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("L.yaml").yaml().build();
+ c.set("foo/k1", " leading");
+ c.commit();
+
+ assertTrue(s.read("L.yaml").contains("\" leading\""));
+ }
+
+ @Test void b20_fromInternalSharedPrefixReusedAcrossSections() throws
Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("M.yaml").yaml().build();
+ c.set("a/b/k1", "v1");
+ c.set("a/c/k2", "v2");
+ c.commit();
+
+ var written = s.read("M.yaml");
+ // 'a' should appear exactly once since the prefix is shared
between 'a/b' and 'a/c'.
+ var firstA = written.indexOf("a:");
+ var lastA = written.lastIndexOf("a:");
+ assertTrue(firstA != -1 && firstA == lastA);
+ }
+
+ @Test void b21_yamlRoundTripPreservesValues() throws Exception {
+ var s = MemoryStore.create().build();
+ var c = Config.create().store(s).name("N.yaml").yaml().build();
+ c.set("alpha/beta/k1", "v1");
+ c.set("alpha/k2", "v2");
+ c.commit();
+
+ var c2 = Config.create().store(s).name("N.yaml").yaml().build();
+ assertEquals("v1", c2.getString("alpha/beta/k1"));
+ assertEquals("v2", c2.getString("alpha/k2"));
+ }
+
+ @Test void b22_roundTripPreservesInlineComment() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("RT.yaml",
+ "foo:",
+ " k1: v1 # original"
+ );
+ var c = Config.create().store(s).name("RT.yaml").build();
+ // Force re-serialization by updating a different key and
committing.
+ c.set("foo/k2", "v2");
+ c.commit();
+
+ var written = s.read("RT.yaml");
+ assertTrue(written.contains("# original"), () -> "Expected
comment to survive round-trip, got:\n" + written);
+ }
+
+ @Test void b23_toInternalAllBlankLineHandled() throws Exception {
+ // A line of only spaces exercises the leadingSpaces "i <
length false" branch.
+ var ini = YamlConfigFormat.INSTANCE.toInternal(
+ "foo:\n"
+ + " \n"
+ + " k1: v1\n"
+ );
+ assertTrue(ini.contains("k1 = v1"));
+ }
+
+ @Test void b24_toInternalUnclosedQuotePassesThrough() throws Exception {
+ // Value starts with a quote but doesn't end with one — unquote
should not strip it.
+ var ini = YamlConfigFormat.INSTANCE.toInternal("k:
\"unmatched\n");
+ assertEquals("k = \"unmatched\n", ini);
+ }
+
+ @Test void b24a_toInternalUnclosedSingleQuotePassesThrough() throws
Exception {
+ // Single-quote variant of b24 to hit the second clause of the
unquote check.
+ var ini = YamlConfigFormat.INSTANCE.toInternal("k:
'unmatched\n");
+ assertEquals("k = 'unmatched\n", ini);
+ }
+
+ @Test void b24b_toInternalEmptyLineAtEnd() throws Exception {
+ // Trailing empty line exercises the leadingSpaces empty-string
path.
+ var ini = YamlConfigFormat.INSTANCE.toInternal("foo:\n k1:
v1\n\n");
+ assertTrue(ini.contains("k1 = v1"));
+ }
+
+ @Test void b25_configToMapMatchesNestedYamlInput() throws Exception {
+ var s = MemoryStore.create().build();
+ s.update("O.yaml",
+ "foo:",
+ " bar:",
+ " k1: v1",
+ " k2: v2"
+ );
+ var c = Config.create().store(s).name("O.yaml").build();
+
+ assertJson("{'':{},'foo/bar':{k1:'v1'},foo:{k2:'v2'}}",
c.toMap());
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
index 0b68495f96..905ff3eb8d 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
@@ -29,7 +29,6 @@ import java.util.stream.*;
import org.apache.juneau.*;
import org.apache.juneau.commons.http.*;
import org.apache.juneau.http.header.*;
-import org.apache.juneau.ng.http.part.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
index a3697ffea3..c6b2bbec17 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
@@ -19,7 +19,6 @@ package org.apache.juneau.ng.rest;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.*;
-import java.util.function.*;
import java.util.stream.*;
import org.apache.juneau.*;
@@ -37,7 +36,6 @@ import org.apache.juneau.ng.rest.client.jetty.*;
import org.apache.juneau.ng.rest.client.okhttp.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.servlet.*;
-import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -296,7 +294,7 @@ class NgRemoteInterfaceTransport_Test extends TestBase {
@MethodSource("transports")
void e01_voidReturn_succeeds(String name, TransportSupplier ts) throws
Exception {
try (var c = buildClient(ts)) {
- c.proxy().noContent(); // expects no exception
+ assertDoesNotThrow(() -> c.proxy().noContent());
}
}