1919import com .fasterxml .jackson .databind .ObjectMapper ;
2020import com .fasterxml .jackson .databind .node .JsonNodeFactory ;
2121import com .fasterxml .jackson .databind .node .ObjectNode ;
22- import com .networknt .schema .Error ;
23- import com .networknt .schema .Schema ;
24- import com .networknt .schema .SchemaLocation ;
25- import com .networknt .schema .SchemaRegistry ;
2622import com .predic8 .membrane .annot .Grammar ;
2723import com .predic8 .membrane .annot .MCAttribute ;
28- import com .predic8 .membrane .annot .MCElement ;
2924import com .predic8 .membrane .annot .MCTextContent ;
3025import com .predic8 .membrane .annot .beanregistry .BeanDefinition ;
3126import com .predic8 .membrane .annot .beanregistry .BeanLifecycleManager ;
3227import com .predic8 .membrane .annot .beanregistry .BeanRegistry ;
33- import com .predic8 .membrane .annot .beanregistry .BeanRegistryAware ;
34- import jakarta .annotation .PostConstruct ;
35- import jakarta .annotation .PreDestroy ;
3628import org .jetbrains .annotations .NotNull ;
3729import org .slf4j .Logger ;
3830import org .slf4j .LoggerFactory ;
39- import org .springframework .util .ReflectionUtils ;
4031
4132import java .io .IOException ;
4233import java .io .InputStream ;
43- import java .lang .annotation .Annotation ;
4434import java .lang .reflect .InvocationTargetException ;
4535import java .lang .reflect .Method ;
4636import java .util .ArrayList ;
4737import java .util .Iterator ;
4838import java .util .List ;
39+ import java .util .Objects ;
4940
50- import static com .networknt .schema .SpecificationVersion .DRAFT_2020_12 ;
5141import static com .predic8 .membrane .annot .yaml .McYamlIntrospector .*;
5242import static com .predic8 .membrane .annot .yaml .MethodSetter .getMethodSetter ;
5343import static com .predic8 .membrane .annot .yaml .NodeValidationUtils .*;
44+ import static com .predic8 .membrane .annot .yaml .YamlParsingUtils .*;
5445import static java .nio .charset .StandardCharsets .UTF_8 ;
5546import static java .util .List .of ;
5647import static java .util .UUID .randomUUID ;
57- import static org .springframework .util .ReflectionUtils .doWithMethods ;
5848
5949public class GenericYamlParser {
6050 private static final Logger log = LoggerFactory .getLogger (GenericYamlParser .class );
@@ -145,16 +135,6 @@ private static String getBeanType(JsonNode jsonNode) {
145135 return jsonNode .fieldNames ().next ();
146136 }
147137
148- private static void validate (Grammar grammar , JsonNode input ) throws YamlSchemaValidationException {
149- Schema schema = SchemaRegistry .withDefaultDialect (DRAFT_2020_12 , builder -> {}).getSchema (SchemaLocation .of (grammar .getSchemaLocation ()));
150- schema .initializeValidators ();
151- List <Error > errors = schema .validate (input );
152- if (!errors .isEmpty ()) {
153- throw new YamlSchemaValidationException ("Invalid YAML." , errors );
154- }
155- }
156-
157-
158138 /**
159139 * Parse a top-level Membrane resource of the given {@code kind}.
160140 * <p>Ensures the node contains exactly one key (the kind), resolves the Java class via the
@@ -185,49 +165,27 @@ public static Class<?> decideClazz(String kind, Grammar grammar, JsonNode node)
185165 public static <T > T createAndPopulateNode (ParsingContext <?> ctx , Class <T > clazz , JsonNode node ) throws ParsingException {
186166 try {
187167 T configObj = clazz .getConstructor ().newInstance ();
168+
169+ // when this is a list, we are on a @MCElement(..., noEnvelope=true)
188170 if (node .isArray ()) {
189- // when this is a list, we are on a @MCElement(..., noEnvelope=true)
190- Method method = getSingleChildSetter (clazz );
191- method .invoke (configObj , parseListExcludingStartEvent (ctx , node ));
192- return configObj ;
171+ return handlePostConstructAndPreDestroy (ctx , handleNoEnvelopeList (ctx , clazz , node , configObj ));
193172 }
194173
195174 // scalar inline form for @MCElement(collapsed=true)
196175 if (isCollapsed (clazz )) {
197- if (node .isNull ()) {
198- throw new ParsingException ("Collapsed element must not be null." , node );
199- }
200- if (node .isArray () || node .isObject ()) {
201- throw new ParsingException ("Element is collapsed; expected an inline scalar value, not " +
202- (node .isArray () ? "an array" : "an object" ) + "." , node );
203- }
204- applyCollapsedScalar (clazz , node , configObj );
205- return handlePostConstructAndPreDestroy (ctx , configObj );
176+ return handleCollapsed (ctx , clazz , node , configObj );
206177 }
207178 ensureMappingStart (node );
208- if (isNoEnvelope (clazz ))
209- throw new RuntimeException ("Class " + clazz .getName () + " is annotated with @MCElement(noEnvelope=true), but the YAML/JSON structure does not contain a list." );
179+ if (isNoEnvelope (clazz )) throw new ParsingException ("Class %s is annotated with @MCElement(noEnvelope=true), but the YAML/JSON structure does not contain a list." .formatted (clazz .getName ()), node );
210180
211181 JsonNode refNode = node .get ("$ref" );
212182 if (refNode != null ) {
213183 applyObjectLevelRef (ctx , clazz , node , refNode , configObj );
214184 }
215185
216186 List <Method > required = findRequiredSetters (clazz );
187+ populateObjectFields (ctx , clazz , node , required , configObj );
217188
218- for (Iterator <String > it = node .fieldNames (); it .hasNext (); ) {
219- String key = it .next ();
220- if ("$ref" .equals (key ))
221- continue ;
222-
223- try {
224- MethodSetter methodSetter = getMethodSetter (ctx , clazz , key );
225- required .remove (methodSetter .getSetter ());
226- methodSetter .setSetter (configObj , ctx , node , key );
227- } catch (Throwable cause ) {
228- throw new ParsingException (cause , node .get (key ));
229- }
230- }
231189 if (!required .isEmpty ())
232190 throw new ParsingException ("Missing required fields: " + required .stream ().map (McYamlIntrospector ::getSetterName ).toList (), node );
233191 return handlePostConstructAndPreDestroy (ctx , configObj );
@@ -244,6 +202,34 @@ public static <T> T createAndPopulateNode(ParsingContext<?> ctx, Class<T> clazz,
244202 }
245203 }
246204
205+ private static <T > void populateObjectFields (ParsingContext <?> ctx , Class <T > clazz , JsonNode node , List <Method > required , T configObj ) {
206+ for (Iterator <String > it = node .fieldNames (); it .hasNext (); ) {
207+ String key = it .next ();
208+ if ("$ref" .equals (key ))
209+ continue ;
210+
211+ try {
212+ MethodSetter methodSetter = getMethodSetter (ctx , clazz , key );
213+ required .remove (methodSetter .getSetter ());
214+ methodSetter .setSetter (configObj , ctx , node , key );
215+ } catch (Throwable cause ) {
216+ throw new ParsingException (cause , node .get (key ));
217+ }
218+ }
219+ }
220+
221+ private static <T > @ NotNull T handleCollapsed (ParsingContext <?> ctx , Class <T > clazz , JsonNode node , T configObj ) {
222+ if (node .isNull ()) throw new ParsingException ("Collapsed element must not be null." , node );
223+ if (node .isArray () || node .isObject ()) throw new ParsingException ("Element is collapsed; expected an inline scalar value, not an %s." .formatted ((node .isArray () ? "array" : "object" )), node );
224+ applyCollapsedScalar (clazz , node , configObj );
225+ return handlePostConstructAndPreDestroy (ctx , configObj );
226+ }
227+
228+ private static <T > T handleNoEnvelopeList (ParsingContext <?> ctx , Class <T > clazz , JsonNode node , T configObj ) throws IllegalAccessException , InvocationTargetException {
229+ getSingleChildSetter (clazz ).invoke (configObj , parseListExcludingStartEvent (ctx , node ));
230+ return configObj ;
231+ }
232+
247233 private static List <BeanDefinition > extractComponentBeanDefinitions (JsonNode componentsNode ) {
248234 if (componentsNode == null || componentsNode .isNull ())
249235 return of ();
@@ -341,54 +327,23 @@ private static Object parseMapToObj(ParsingContext<?> ctx, JsonNode node, String
341327 return createAndPopulateNode (ctx .updateContext (key ), ctx .resolveClass (key ), node );
342328 }
343329
344- /**
345- * Calls the @PostConstruct method on the bean and returns it. If there are @PreDestroy methods, they will be
346- * registered within the registry.
347- */
348- private static <T > T handlePostConstructAndPreDestroy (ParsingContext <?> ctx , T bean ) {
349- if (bean instanceof BeanRegistryAware beanRegistryAware ) {
350- beanRegistryAware .setRegistry (ctx .registry ());
351- }
352- doWithMethods (bean .getClass (), method -> {
353- if (method .isAnnotationPresent (PostConstruct .class )) {
354- try {
355- method .setAccessible (true );
356- method .invoke (bean );
357- } catch (InvocationTargetException e ) {
358- throw new RuntimeException (e .getTargetException ());
359- } catch (IllegalAccessException | IllegalArgumentException e ) {
360- throw new RuntimeException (e );
361- }
362- }
363- if (method .isAnnotationPresent (PreDestroy .class )) {
364- method .setAccessible (true );
365- ctx .registry ().addPreDestroyCallback (bean , method );
366- }
367- });
368- return bean ;
369- }
370-
371330 private static <T > void applyCollapsedScalar (Class <T > clazz , JsonNode node , T target ) {
372331 if (node == null || node .isNull ()) {
373332 throw new ParsingException ("Collapsed element must not be null." , node );
374333 }
375334
335+ // Collapsed classes can only have one matching setter (ensured by SpringConfigurationXSDGeneratingAnnotationProcessor)
376336 Method attributeSetter = findSingleSetterOrNullForAnnotation (clazz , MCAttribute .class );
377337 Method textSetter = findSingleSetterOrNullForAnnotation (clazz , MCTextContent .class );
378338
379- if ((attributeSetter == null ) == (textSetter == null )) {
380- // both null or both non-null -> invalid
381- throw new ParsingException ("@MCElement(collapsed=true) requires exactly one @MCAttribute setter OR exactly one @MCTextContent setter." , node );
382- }
383-
384339 Method setter = (attributeSetter != null ) ? attributeSetter : textSetter ;
385- Class <?> paramType = setter .getParameterTypes ()[0 ];
340+ Class <?> paramType = Objects . requireNonNull ( setter ) .getParameterTypes ()[0 ];
386341
387342 Object value ;
388343 try {
389344 value = SCALAR_MAPPER .convertValue (node , paramType );
390345 } catch (IllegalArgumentException e ) {
391- throw new ParsingException ("Cannot convert inline value to " + paramType .getSimpleName () + "." , node );
346+ throw new ParsingException ("Cannot convert inline value to %s." . formatted ( paramType .getSimpleName ()) , node );
392347 }
393348
394349 try {
@@ -401,22 +356,6 @@ private static <T> void applyCollapsedScalar(Class<T> clazz, JsonNode node, T ta
401356 }
402357 }
403358
404- private static Method findSingleSetterOrNullForAnnotation (Class <?> clazz , Class <? extends java .lang .annotation .Annotation > annotation ) {
405- List <Method > setters = new ArrayList <>();
406- doWithMethods (clazz , m -> {
407- if (m .isAnnotationPresent (annotation ) && m .getParameterCount () == 1 ) {
408- setters .add (m );
409- }
410- });
411359
412- if (setters .isEmpty ()) return null ;
413- if (setters .size () != 1 ) {
414- throw new ParsingException (
415- "Multiple @%s setters found for collapsed element." .formatted (annotation .getSimpleName ()),
416- JsonNodeFactory .instance .nullNode ()
417- );
418- }
419- return setters .getFirst ();
420- }
421360
422361}
0 commit comments