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 parseListIncludingStartEvent(ParsingContext contex } public static List parseListIncludingStartEvent(ParsingContext pc, JsonNode node, Class elemType) throws ConfigurationParsingException { - ensureArray(pc,node); + ensureArray(pc, node); return parseListExcludingStartEvent(pc, node, elemType); } @@ -347,7 +348,7 @@ public static List parseListIncludingStartEvent(ParsingContext pc, Js * delegating to {@link #parseMapToObj(ParsingContext, JsonNode, String)}. */ private static Object parseMapToObj(ParsingContext pc, JsonNode node) throws ConfigurationParsingException { - ensureSingleKey(pc,node); + ensureSingleKey(pc, node); String key = node.fieldNames().next(); return parseMapToObj(pc, node.get(key), key); } @@ -420,7 +421,8 @@ private static Object parseListItem(ParsingContext ctx, JsonNode item, Class< } private static Object parseInlineListItem(ParsingContext ctx, JsonNode node, Class elemType) { - if (elemType == null) throw new ConfigurationParsingException("Inline list item form requires a typed list element."); + if (elemType == null) + throw new ConfigurationParsingException("Inline list item form requires a typed list element."); if (isScalarElementType(elemType)) { if (node.isObject() || node.isArray()) { throw new ConfigurationParsingException( @@ -436,10 +438,10 @@ private static Object parseInlineListItem(ParsingContext ctx, JsonNode node, private static boolean isScalarElementType(Class t) { return t == String.class - || t == Boolean.class - || t == Character.class - || Number.class.isAssignableFrom(t) - || t.isEnum(); + || t == Boolean.class + || t == Character.class + || Number.class.isAssignableFrom(t) + || t.isEnum(); } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -450,7 +452,8 @@ private static Object coerceScalarListItem(JsonNode node, Class elemType) { String raw = node.asText(); try { return Enum.valueOf((Class) elemType, raw); - } catch (IllegalArgumentException ignored) {} + } catch (IllegalArgumentException ignored) { + } try { return Enum.valueOf((Class) elemType, raw.toUpperCase(ROOT)); } catch (IllegalArgumentException e) { From dfe0c1d944a7f981108844aebf8a7cdb0fa6ee80 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Tue, 24 Feb 2026 13:59:14 +0100 Subject: [PATCH 3/3] fix(parser): improve exception handling in `GenericYamlParser` - Added additional context to `ConfigurationParsingException` to enhance debugging (`$ref` key and its context). --- .../java/com/predic8/membrane/annot/yaml/GenericYamlParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1e49f8b72a..d06f307871 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 @@ -312,7 +312,7 @@ private static void applyObjectLevelRef(ParsingContext ctx, Class pare } catch (RuntimeException e) { throw new ConfigurationParsingException( "Referenced component '%s' (type '%s') is not allowed in '%s'." - .formatted(refNode.asText(), refKey, ctx.getContext())); + .formatted(refNode.asText(), refKey, ctx.getContext()),e,ctx.key("$ref")); } catch (Throwable t) { throw new ConfigurationParsingException(t); }