Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* </p>
*
* @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 {
Expand All @@ -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(
Expand Down Expand Up @@ -118,8 +119,9 @@ private static void validateAgainstSchema(Grammar grammar, JsonNode jsonNode, Js
* <li>Validates each document against the JSON Schema provided by {@code grammar}.</li>
* <li>Emits helpful line/column locations for malformed multi-document input.</li>
* </ul>
*
* @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<BeanDefinition> parseMembraneResources(@NotNull InputStream resource, Grammar grammar) throws IOException {
Expand Down Expand Up @@ -150,18 +152,18 @@ private static String getBeanType(ParsingContext<?> ctx, JsonNode jsonNode) {
* grammar and delegates to {@link #createAndPopulateNode(ParsingContext, Class, JsonNode)}.</p>
*/
public static <R extends BeanRegistry & BeanLifecycleManager> 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;
}
Expand All @@ -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> T createAndPopulateNode(ParsingContext<?> pc, Class<T> clazz, JsonNode node) throws ConfigurationParsingException {
Expand All @@ -189,7 +191,7 @@ public static <T> T createAndPopulateNode(ParsingContext<?> pc, Class<T> 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");
Expand All @@ -215,9 +217,8 @@ public static <T> T createAndPopulateNode(ParsingContext<?> pc, Class<T> 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);
}
}
Expand All @@ -234,9 +235,8 @@ private static <T> void populateObjectFields(ParsingContext<?> ctx, Class<T> 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;
Expand All @@ -246,7 +246,8 @@ private static <T> void populateObjectFields(ParsingContext<?> ctx, Class<T> cla

private static <T> @NotNull T handleCollapsed(ParsingContext<?> ctx, Class<T> 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);
}
Expand All @@ -272,7 +273,7 @@ private static List<BeanDefinition> 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: { <kind>: <body> }
Expand Down Expand Up @@ -311,7 +312,7 @@ private static <T> void applyObjectLevelRef(ParsingContext<?> ctx, Class<T> 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);
}
Expand All @@ -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"));
}
}

Expand All @@ -330,7 +331,7 @@ public static List<Object> parseListIncludingStartEvent(ParsingContext<?> contex
}

public static List<Object> parseListIncludingStartEvent(ParsingContext<?> pc, JsonNode node, Class<?> elemType) throws ConfigurationParsingException {
ensureArray(pc,node);
ensureArray(pc, node);
return parseListExcludingStartEvent(pc, node, elemType);
}

Expand All @@ -347,7 +348,7 @@ public static List<Object> 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);
}
Expand Down Expand Up @@ -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(
Expand All @@ -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"})
Expand All @@ -450,7 +452,8 @@ private static Object coerceScalarListItem(JsonNode node, Class<?> elemType) {
String raw = node.asText();
try {
return Enum.valueOf((Class<? extends Enum>) elemType, raw);
} catch (IllegalArgumentException ignored) {}
} catch (IllegalArgumentException ignored) {
}
try {
return Enum.valueOf((Class<? extends Enum>) elemType, raw.toUpperCase(ROOT));
} catch (IllegalArgumentException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading