22
33import com .github .flexca .enot .core .EnotContext ;
44import com .github .flexca .enot .core .exception .EnotParsingException ;
5+ import com .github .flexca .enot .core .parser .context .ParsingContext ;
56import com .github .flexca .enot .core .registry .EnotElementBodyResolver ;
67import com .github .flexca .enot .core .registry .EnotElementSpecification ;
78import com .github .flexca .enot .core .registry .EnotTypeSpecification ;
1617
1718import java .util .*;
1819
20+ /**
21+ * Parses eNot template JSON into a list of {@link EnotElement} instances.
22+ *
23+ * <p>The parser accepts a JSON object (single root element) or a JSON array
24+ * (multiple root elements) and walks the tree recursively, resolving element
25+ * types via the {@link com.github.flexca.enot.core.registry.EnotRegistry},
26+ * validating attributes and body structure, and delegating dynamic body
27+ * resolution to any registered {@link EnotElementBodyResolver}.</p>
28+ *
29+ * <p>Cyclic-dependency detection is handled transparently: a fresh
30+ * {@link ParsingContext} is created for each top-level {@link #parse(String, EnotContext)}
31+ * call, and a snapshot copy is passed to each body resolver so that sibling
32+ * branches do not interfere with each other's tracking sets.</p>
33+ *
34+ * <p>All parse errors are collected into {@link EnotJsonError} entries and
35+ * reported together via a single {@link EnotParsingException}, making it easy
36+ * to surface multiple problems in one pass.</p>
37+ */
1938public class EnotParser {
2039
2140 public static final String ENOT_ELEMENT_TYPE_NAME = "type" ;
@@ -33,7 +52,45 @@ public EnotParser(ObjectMapper objectMapper) {
3352 this .objectMapper = objectMapper ;
3453 }
3554
55+ /**
56+ * Parses {@code json} into a list of {@link EnotElement} instances using a
57+ * fresh {@link ParsingContext}.
58+ *
59+ * <p>This is the standard entry point for top-level parsing. A new
60+ * {@link ParsingContext} is created automatically so that cyclic-dependency
61+ * detection starts from an empty state.</p>
62+ *
63+ * @param json the eNot template as a JSON string; must not be blank
64+ * @param enotContext the registry and shared services for this parse run
65+ * @return a non-empty list of parsed root elements
66+ * @throws EnotParsingException if the input is blank, not valid JSON, or
67+ * contains structural or type errors
68+ */
3669 public List <EnotElement > parse (String json , EnotContext enotContext ) throws EnotParsingException {
70+ ParsingContext parsingContext = new ParsingContext ();
71+ return parse (json , enotContext , parsingContext );
72+ }
73+
74+ /**
75+ * Parses {@code json} into a list of {@link EnotElement} instances using the
76+ * supplied {@link ParsingContext}.
77+ *
78+ * <p>Use this overload when parsing is initiated from within an
79+ * {@link EnotElementBodyResolver} (e.g. {@code system/reference} resolution),
80+ * so that the caller's already-populated context — carrying the set of
81+ * composite identifiers currently being resolved — is passed through.
82+ * This allows cycle detection to span across template boundaries.</p>
83+ *
84+ * @param json the eNot template as a JSON string; must not be blank
85+ * @param enotContext the registry and shared services for this parse run
86+ * @param parsingContext the active parsing context propagated from the caller;
87+ * must be a {@link ParsingContext#copy() copy} so that
88+ * sibling branches remain independent
89+ * @return a non-empty list of parsed root elements
90+ * @throws EnotParsingException if the input is blank, not valid JSON, or
91+ * contains structural or type errors
92+ */
93+ public List <EnotElement > parse (String json , EnotContext enotContext , ParsingContext parsingContext ) throws EnotParsingException {
3794
3895 String currentPath = "" ;
3996 if (StringUtils .isBlank (json )) {
@@ -55,7 +112,7 @@ public List<EnotElement> parse(String json, EnotContext enotContext) throws Enot
55112
56113 if (rootNode .isArray ()) {
57114 try {
58- elements .addAll (parseElements (rootNode .asArray (), currentPath , jsonErrors , enotContext ));
115+ elements .addAll (parseElements (rootNode .asArray (), currentPath , jsonErrors , enotContext , parsingContext ));
59116 } catch (Exception e ) {
60117 cause = e ;
61118 jsonErrors .add (EnotJsonError .of (currentPath , e .getMessage ()));
@@ -65,7 +122,8 @@ public List<EnotElement> parse(String json, EnotContext enotContext) throws Enot
65122 }
66123 } else if (rootNode .isObject ()) {
67124 try {
68- Optional <EnotElement > element = parseElement (rootNode .asObject (), currentPath , jsonErrors , enotContext );
125+ Optional <EnotElement > element = parseElement (rootNode .asObject (), currentPath , jsonErrors , enotContext ,
126+ parsingContext );
69127 element .ifPresent (elements ::add );
70128 } catch (Exception e ) {
71129 cause = e ;
@@ -87,14 +145,15 @@ public List<EnotElement> parse(String json, EnotContext enotContext) throws Enot
87145 }
88146
89147 private List <EnotElement > parseElements (ArrayNode elementsArray , String parentPath , List <EnotJsonError > jsonErrors ,
90- EnotContext enotContext ) {
148+ EnotContext enotContext , ParsingContext parsingContext ) {
91149
92150 List <EnotElement > elements = new ArrayList <>(elementsArray .size ());
93151 for (int i = 0 ; i < elementsArray .size (); i ++) {
94152 JsonNode itemNode = elementsArray .get (i );
95153 String currentPath = parentPath + "/" + i ;
96154 if (itemNode .isObject ()) {
97- Optional <EnotElement > element = parseElement (itemNode .asObject (), currentPath , jsonErrors , enotContext );
155+ Optional <EnotElement > element = parseElement (itemNode .asObject (), currentPath , jsonErrors , enotContext ,
156+ parsingContext );
98157 element .ifPresent (elements ::add );
99158 } else {
100159 jsonErrors .add (EnotJsonError .of (currentPath , "eNot expecting object, but get " + itemNode .getNodeType ().name ()));
@@ -104,7 +163,7 @@ private List<EnotElement> parseElements(ArrayNode elementsArray, String parentPa
104163 }
105164
106165 private Optional <EnotElement > parseElement (ObjectNode jsonElement , String parentPath , List <EnotJsonError > jsonErrors ,
107- EnotContext enotContext ) {
166+ EnotContext enotContext , ParsingContext parsingContext ) {
108167
109168 JsonNode typeNode = jsonElement .get (ENOT_ELEMENT_TYPE_NAME );
110169 if (typeNode == null ) {
@@ -150,16 +209,11 @@ private Optional<EnotElement> parseElement(ObjectNode jsonElement, String parent
150209 }
151210 EnotElementBodyResolver bodyResolver = elementSpecification .getBodyResolver ();
152211 if (bodyResolver == null ) {
153- Optional <Object > elementBody = extractElementBody (jsonElement , parentPath , jsonErrors , enotContext );
212+ Optional <Object > elementBody = extractElementBody (jsonElement , parentPath , jsonErrors , enotContext , parsingContext );
154213 elementBody .ifPresent (element ::setBody );
155214 } else {
156215 try {
157-
158- String compositeIdentifier = bodyResolver .getUniqueCompositeIdentifier (element );
159- if (StringUtils .isNotBlank (compositeIdentifier )) {
160- // TODO: detect cyclic dependency
161- }
162- element .setBody (bodyResolver .resolveBody (element , enotContext ));
216+ element .setBody (bodyResolver .resolveBody (element , enotContext , parsingContext .copy ()));
163217 } catch (Exception e ) {
164218 jsonErrors .add (EnotJsonError .of (parentPath + "/" + ENOT_ELEMENT_BODY_NAME ,
165219 "failure during resolving element body, reason: " + e .getMessage ()));
@@ -224,7 +278,7 @@ private Map<EnotAttribute, Object> extractElementAttributes(ObjectNode jsonEleme
224278 }
225279
226280 private Optional <Object > extractElementBody (ObjectNode jsonElement , String parentPath , List <EnotJsonError > jsonErrors ,
227- EnotContext enotContext ) {
281+ EnotContext enotContext , ParsingContext parsingContext ) {
228282
229283 String currentPath = parentPath + "/" + ENOT_ELEMENT_BODY_NAME ;
230284
@@ -237,7 +291,7 @@ private Optional<Object> extractElementBody(ObjectNode jsonElement, String paren
237291 return Optional .empty ();
238292 }
239293 if (bodyArrayNode .get (0 ).isObject ()) {
240- List <EnotElement > elements = parseElements (bodyArrayNode , currentPath , jsonErrors , enotContext );
294+ List <EnotElement > elements = parseElements (bodyArrayNode , currentPath , jsonErrors , enotContext , parsingContext );
241295 return CollectionUtils .isEmpty (elements ) ? Optional .empty () : Optional .of (elements );
242296 } else {
243297 List <Object > primitiveValues = new ArrayList <>();
@@ -255,7 +309,7 @@ private Optional<Object> extractElementBody(ObjectNode jsonElement, String paren
255309 return CollectionUtils .isEmpty (primitiveValues ) ? Optional .empty () : Optional .of (primitiveValues );
256310 }
257311 } else if (bodyNode .isObject ()) {
258- Optional <EnotElement > element = parseElement (bodyNode .asObject (), currentPath , jsonErrors , enotContext );
312+ Optional <EnotElement > element = parseElement (bodyNode .asObject (), currentPath , jsonErrors , enotContext , parsingContext );
259313 return element .isEmpty () ? Optional .empty () : Optional .of (element .get ());
260314 } else {
261315 Optional <Object > objectBody = extractPrimitiveValue (bodyNode );
0 commit comments