From e732cb0d020e8a039e65824fb817bdba389eb2f1 Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Tue, 24 Feb 2026 12:23:53 +0100
Subject: [PATCH 1/3] refactor(validation): enhance error message for multiple
key detection
- Updated `ConfigurationParsingException` message to provide clearer guidance on separating APIs and configuration using `---`.
---
.../com/predic8/membrane/annot/yaml/NodeValidationUtils.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/NodeValidationUtils.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/NodeValidationUtils.java
index 7edbd099a6..92576c41a4 100644
--- a/annot/src/main/java/com/predic8/membrane/annot/yaml/NodeValidationUtils.java
+++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/NodeValidationUtils.java
@@ -27,7 +27,7 @@ public static void ensureMappingStart(JsonNode node) throws ConfigurationParsing
public static void ensureSingleKey(ParsingContext> ctx, JsonNode node) {
ensureMappingStart(node);
if (node.size() != 1) {
- var e = new ConfigurationParsingException("Expected exactly one key but there are %d.".formatted(node.size()));
+ var e = new ConfigurationParsingException("Expected exactly one key but there are %d. Separate APIs and configuration by ---".formatted(node.size()));
e.setParsingContext(ctx);
throw e;
}
From 77e7e85079e5f6598c1e8088d3820a2ffe758090 Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Tue, 24 Feb 2026 13:56:25 +0100
Subject: [PATCH 2/3] refactor(parser): improve code formatting, readability,
and exception messages
- Adjusted spacing and formatting for better consistency across methods.
- Enhanced clarity of exception messages in `ConfigurationParsingException` and improved handling of multi-document YAML scenarios.
- Streamlined error logs for better debugging during configuration parsing.
---
.../BeanRegistryImplementation.java | 7 ++-
.../annot/yaml/GenericYamlParser.java | 57 ++++++++++---------
2 files changed, 34 insertions(+), 30 deletions(-)
diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java
index 3348a99c3c..38e62cc9d8 100644
--- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java
+++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java
@@ -17,15 +17,16 @@
import com.predic8.membrane.annot.yaml.*;
import org.jetbrains.annotations.*;
import org.slf4j.*;
+import org.springframework.beans.factory.xml.*;
import javax.annotation.concurrent.*;
-import java.io.IOException;
-import java.lang.reflect.Method;
+import java.io.*;
+import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
-import static com.predic8.membrane.annot.yaml.WatchAction.ADDED;
+import static com.predic8.membrane.annot.yaml.WatchAction.*;
/**
* TODO:
diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java
index 18c22fe137..1e49f8b72a 100644
--- a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java
+++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java
@@ -64,8 +64,9 @@ public class GenericYamlParser {
* turned into a {@link BeanDefinition}. Validation errors are mapped back to line/column
* numbers using {@link JsonLocationMap} to produce helpful error messages.
*
+ *
* @param grammar provides schema location and Java type resolution
- * @param yaml the raw YAML content (may contain multi-document stream)
+ * @param yaml the raw YAML content (may contain multi-document stream)
* @throws IOException if schema loading or validation fails
*/
public GenericYamlParser(Grammar grammar, String yaml) throws IOException {
@@ -81,10 +82,10 @@ public GenericYamlParser(Grammar grammar, String yaml) throws IOException {
// Deactivated temporarily to get better error messages
//validateAgainstSchema(grammar, jsonNode, jsonLocationMap);
- var pc = new ParsingContext<>("",null,grammar, jsonNode, "$",null);
+ var pc = new ParsingContext<>("", null, grammar, jsonNode, "$", null);
if ("components".equals(getBeanType(pc, jsonNode))) {
- beanDefs.addAll(extractComponentBeanDefinitions(pc.addPath(".components"),jsonNode.get("components")));
+ beanDefs.addAll(extractComponentBeanDefinitions(pc.addPath(".components"), jsonNode.get("components")));
}
beanDefs.add(new BeanDefinition(
@@ -118,8 +119,9 @@ private static void validateAgainstSchema(Grammar grammar, JsonNode jsonNode, Js
*
Validates each document against the JSON Schema provided by {@code grammar}.
*
Emits helpful line/column locations for malformed multi-document input.
*
+ *
* @param resource the input stream to parse. The method takes care of closing the stream.
- * @param grammar the grammar to use for type resolution and schema location
+ * @param grammar the grammar to use for type resolution and schema location
* @return list of parsed bean definitions
*/
public static List parseMembraneResources(@NotNull InputStream resource, Grammar grammar) throws IOException {
@@ -150,18 +152,18 @@ private static String getBeanType(ParsingContext> ctx, JsonNode jsonNode) {
* grammar and delegates to {@link #createAndPopulateNode(ParsingContext, Class, JsonNode)}.
*/
public static Object readMembraneObject(String kind, Grammar grammar, JsonNode node, R registry) throws ConfigurationParsingException {
- return createAndPopulateNode(new ParsingContext<>(kind, registry, grammar,node, "$." + kind,null), decideClazz(kind, grammar, node), node.get(kind));
+ return createAndPopulateNode(new ParsingContext<>(kind, registry, grammar, node, "$." + kind, null), decideClazz(kind, grammar, node), node.get(kind));
}
/**
* Detects the class that will be selected to represent the node in Java.
*/
public static Class> decideClazz(String kind, Grammar grammar, JsonNode node) {
- ensureSingleKey(new ParsingContext("",null, grammar,node,"$",null),node);
+ ensureSingleKey(new ParsingContext("", null, grammar, node, "$", null), node);
Class> clazz = grammar.getElement(kind);
if (clazz == null) {
- var pc = new ParsingContext("", null,grammar,node,"$",null).key(kind);
- throw new ConfigurationParsingException("Did not find java class for kind '%s'.".formatted(kind),null,pc);
+ var pc = new ParsingContext("", null, grammar, node, "$", null).key(kind);
+ throw new ConfigurationParsingException("Did not find java class for kind '%s'.".formatted(kind), null, pc);
}
return clazz;
}
@@ -170,7 +172,7 @@ public static Class> decideClazz(String kind, Grammar grammar, JsonNode node)
* Creates and populates an instance of {@code clazz} from the given YAML/JSON node.
* - Arrays: only valid for {@code @MCElement(noEnvelope=true)}; items are parsed and passed to the single {@code @MCChildElement} list setter.
* - Objects: each field is mapped to a setter resolved by {@link MethodSetter#getMethodSetter(ParsingContext, Class, String)};
- * values are produced by {@link MethodSetter#getMethodSetter(ParsingContext, Class, String)}. A top-level {@code "$ref"} injects a previously defined bean.
+ * values are produced by {@link MethodSetter#getMethodSetter(ParsingContext, Class, String)}. A top-level {@code "$ref"} injects a previously defined bean.
* All failures are wrapped in a {@link ConfigurationParsingException} with location information.
*/
public static T createAndPopulateNode(ParsingContext> pc, Class clazz, JsonNode node) throws ConfigurationParsingException {
@@ -189,7 +191,7 @@ public static T createAndPopulateNode(ParsingContext> pc, Class clazz,
ensureMappingStart(node);
if (isNoEnvelope(clazz)) {
log.error("Class {} is annotated with @MCElement(noEnvelope=true), but the YAML/JSON structure does not contain a list.", clazz.getName());
- throw new ConfigurationParsingException("Class %s is annotated with @MCElement(noEnvelope=true), but the YAML/JSON structure does not contain a list.".formatted(clazz.getName()),null,pc);
+ throw new ConfigurationParsingException("Class %s is annotated with @MCElement(noEnvelope=true), but the YAML/JSON structure does not contain a list.".formatted(clazz.getName()), null, pc);
}
JsonNode refNode = node.get("$ref");
@@ -215,9 +217,8 @@ public static T createAndPopulateNode(ParsingContext> pc, Class clazz,
if (e.getParsingContext() == null)
e.setParsingContext(pc);
throw e;
- }
- catch (Throwable cause) {
- log.debug("",cause);
+ } catch (Throwable cause) {
+ log.debug("", cause);
throw new ConfigurationParsingException(cause);
}
}
@@ -234,9 +235,8 @@ private static void populateObjectFields(ParsingContext> ctx, Class cla
setter.setSetter(configObj, ctx, node, key);
} catch (ConfigurationParsingException e) {
throw e;
- }
- catch (Throwable cause) {
- log.debug("",cause);
+ } catch (Throwable cause) {
+ log.debug("", cause);
var e = new ConfigurationParsingException(cause.getMessage());
e.setParsingContext(ctx.key(key));
throw e;
@@ -246,7 +246,8 @@ private static void populateObjectFields(ParsingContext> ctx, Class cla
private static @NotNull T handleCollapsed(ParsingContext> ctx, Class clazz, JsonNode node, T configObj) {
if (node.isNull()) throw new ConfigurationParsingException("Collapsed element must not be null.");
- if (node.isArray() || node.isObject()) throw new ConfigurationParsingException("Element is collapsed; expected an inline scalar value, not an %s.".formatted((node.isArray() ? "array" : "object")));
+ if (node.isArray() || node.isObject())
+ throw new ConfigurationParsingException("Element is collapsed; expected an inline scalar value, not an %s.".formatted((node.isArray() ? "array" : "object")));
applyCollapsedScalar(clazz, node, configObj);
return handlePostConstructAndPreDestroy(ctx, configObj);
}
@@ -272,7 +273,7 @@ private static List extractComponentBeanDefinitions(ParsingConte
JsonNode def = componentsNode.get(id);
// Each component definition must have exactly one key (the component type)
- ensureSingleKey(pc.addPath("."+id),def);
+ ensureSingleKey(pc.addPath("." + id), def);
String componentKind = def.fieldNames().next();
// Wrap it into a normal top-level node: { : }
@@ -321,7 +322,7 @@ private static Object getReferenced(ParsingContext> ctx, JsonNode refNode) {
try {
return ctx.getRegistry().resolve(refNode.asText());
} catch (RuntimeException e) {
- throw new ConfigurationParsingException(e);
+ throw new ConfigurationParsingException("Cannot resolve reference: " + refNode.asText(),e,ctx.key("$ref"));
}
}
@@ -330,7 +331,7 @@ public static List