From 6f172ab9776e8b2ef1ccacd97f07e0df7e21d770 Mon Sep 17 00:00:00 2001 From: Andy Lowry Date: Wed, 19 Sep 2018 08:43:05 -0400 Subject: [PATCH] [#6] Added callback capability to JsonStateWalker Also reworked terminology so that "walk" now refers to what the walker does to the tree as a whole or to container nodes within it, while "visit" is used for what happens to each node included in the walk. The Disposition class for the result of a visit now includes an optional Callback metnod instance, which will be invoked just prior to finishing up the visit. This means that the walk method can do something while visiting a container node and then undo it after all its children have been visited. We'll use that for managing a set of references already encountered in this walk while visiting an ancestor node, in order to support reference cycle detection. --- .../kaizen/normalizer/ReferenceProcessor.java | 12 +- .../kaizen/normalizer/ReferenceScanner.java | 71 +++--- .../normalizer/util/JsonStateWalker.java | 233 ++++++++++-------- .../kaizen/normalizer/util/StateMachine.java | 33 ++- .../normalizer/test/JsonStateWalkerTest.java | 169 +++++++------ 5 files changed, 273 insertions(+), 245 deletions(-) diff --git a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/ReferenceProcessor.java b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/ReferenceProcessor.java index 3f28004..7a71e4f 100644 --- a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/ReferenceProcessor.java +++ b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/ReferenceProcessor.java @@ -11,7 +11,7 @@ import com.reprezen.kaizen.normalizer.ReferenceScanner.ScanOp; import com.reprezen.kaizen.normalizer.util.JsonCopier; import com.reprezen.kaizen.normalizer.util.JsonStateWalker; -import com.reprezen.kaizen.normalizer.util.JsonStateWalker.AdvancedWalkMethod; +import com.reprezen.kaizen.normalizer.util.JsonStateWalker.AdvancedVisitMethod; import com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition; import com.reprezen.kaizen.normalizer.util.StateMachine; import com.reprezen.kaizen.normalizer.util.StateMachine.State; @@ -73,19 +73,19 @@ private JsonNode buildNormalizedModel(Content topModel) { private void copyOtherElements(JsonNode model, ObjectNode result) { Tracker tracker = machine.tracker(modelState); - OtherElementWalkMethod walkMethod = new OtherElementWalkMethod(result); - new JsonStateWalker(tracker, walkMethod, true, true).walk(model); + OtherElementVisitMethod visitMethod = new OtherElementVisitMethod(result); + new JsonStateWalker(tracker, visitMethod, true, true).walk(model); } - private static class OtherElementWalkMethod & Component> implements AdvancedWalkMethod { + private static class OtherElementVisitMethod & Component> implements AdvancedVisitMethod { private JsonNode target; - public OtherElementWalkMethod(JsonNode copyTo) { + public OtherElementVisitMethod(JsonNode copyTo) { this.target = copyTo; } @Override - public Disposition walk(JsonNode node, State state, E stateValue, List path, JsonPointer pointer) { + public Disposition visit(JsonNode node, State state, E stateValue, List path, JsonPointer pointer) { if (stateValue == V2State.OFFROAD) { JsonCopier.copy(node, target, path); return Disposition.done(); diff --git a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/ReferenceScanner.java b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/ReferenceScanner.java index 35e9a52..e3e8e25 100644 --- a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/ReferenceScanner.java +++ b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/ReferenceScanner.java @@ -11,7 +11,7 @@ import com.reprezen.kaizen.normalizer.Localizer.LocalizedContent; import com.reprezen.kaizen.normalizer.Reference.ReferenceTreatment; import com.reprezen.kaizen.normalizer.util.JsonStateWalker; -import com.reprezen.kaizen.normalizer.util.JsonStateWalker.AdvancedWalkMethod; +import com.reprezen.kaizen.normalizer.util.JsonStateWalker.AdvancedVisitMethod; import com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition; import com.reprezen.kaizen.normalizer.util.StateMachine; import com.reprezen.kaizen.normalizer.util.StateMachine.State; @@ -51,48 +51,42 @@ public JsonNode scan(E start) { public JsonNode scan(State startState) { Tracker tracker = machine.tracker(startState); - Walkers walkers = new Walkers(base, contentManager, options); - AdvancedWalkMethod walkMethod = walkers.getWalkMethod(scanOp); - Optional newNode = new JsonStateWalker(tracker, walkMethod).walk(tree); + Visitors visitors = new Visitors(base, contentManager, options); + AdvancedVisitMethod visitMethod = visitors.getVisitMethod(scanOp); + Optional newNode = new JsonStateWalker(tracker, visitMethod).walk(tree); return newNode.orElse(tree); } - private static class Walkers & Component> { + private static class Visitors & Component> { private Reference base; private ContentManager contentManager; private Options options; - public Walkers(Reference base, ContentManager contentManager, Options options) { + public Visitors(Reference base, ContentManager contentManager, Options options) { this.base = base; this.contentManager = contentManager; this.options = options; } - public AdvancedWalkMethod getWalkMethod(ScanOp scanOp) { + public AdvancedVisitMethod getVisitMethod(ScanOp scanOp) { switch (scanOp) { case LOAD: - return this::loadWalkMethod; + return this::loadVisitMethod; case COMPONENTS: - return this::componentWalkMethod; + return this::componentVisitMethod; case POLICY: - return this::policyWalkMethod; + return this::policyVisitMethod; case NONE: default: - throw new IllegalArgumentException("There is no walk method for a NONE scan oepration"); + throw new IllegalArgumentException("There is no visit method for a NONE scan oepration"); } } /** - * Walk method for the LOAD phase, where model files are filled in by having + * Visit method for the LOAD phase, where model files are filled in by having * their non-conforming references inlined. - *

- * Scan Args: - *

    - *
  1. boolean - whether to fix simple refs
  2. - *
- * */ - public Disposition loadWalkMethod(JsonNode node, State state, E stateValue, List path, + public Disposition loadVisitMethod(JsonNode node, State state, E stateValue, List path, JsonPointer pointer) { if (Reference.isRefNode(node)) { Reference ref = new Reference(getRefString(node).get(), base, stateValue); @@ -105,8 +99,8 @@ public Disposition loadWalkMethod(JsonNode node, State state, E stateValue, L // with an adorned ref node // TODO handle cycles Content toInline = contentManager.load(ref, state); - return toInline.isValid() ? Disposition.rewalk(toInline.copyTree()) - : Disposition.done(toInline.getRef().getRefNode()); + return toInline.isValid() ? Disposition.revisit().withReplacement(toInline.copyTree()) + : Disposition.done().withReplacement(toInline.getRef().getRefNode()); } case MERGE: case INLINE_CONFORMING: @@ -114,24 +108,22 @@ public Disposition loadWalkMethod(JsonNode node, State state, E stateValue, L case RETAIN: case ERROR: // all other refs are copied with adornments - return Disposition.done(ref.getRefNode()); + return Disposition.done().withReplacement(ref.getRefNode()); } } return Disposition.normal(); } /** - * Walk method for the COMPONENTS phase, where local component definitions are + * Visit method for the COMPONENTS phase, where local component definitions are * located and added to the localizer for possible inclusion in the final model. *

* Name collisions are resolved by the localizer. Note that additional component * definitions may be localized in the POLICY phase, since COMPONENTS phase only * localizes definitions in their standard containers in top-level source * models. - *

- * Scan args: none */ - public Disposition componentWalkMethod(JsonNode node, State state, E stateValue, List path, + public Disposition componentVisitMethod(JsonNode node, State state, E stateValue, List path, JsonPointer pointer) { if (stateValue.isDefiningSite()) { if (stateValue.hasMergeSemantics()) { @@ -157,7 +149,7 @@ public Disposition componentWalkMethod(JsonNode node, State state, E stateVal } /** - * Walk method for the POLICY phase, where conforming references are either + * Visit method for the POLICY phase, where conforming references are either * inlined or retained, accorinding to retention policy in force. *

* Conforming references are resolved for the first time in this phase, and such @@ -176,25 +168,20 @@ public Disposition componentWalkMethod(JsonNode node, State state, E stateVal * component definitions to the normalized model. Of course conforming * references contained in newly loaded content may still contribute new * definitions through localization. - *

- * Scan args: - *

    - *
  1. boolean - whether to fix simple refs when scanning new content inlined or - * localized content
  2. */ - public Disposition policyWalkMethod(JsonNode node, State state, E stateValue, List path, + public Disposition policyVisitMethod(JsonNode node, State state, E stateValue, List path, JsonPointer pointer) { if (Reference.isRefNode(node) && stateValue.isConformingSite()) { Reference ref = new Reference(getRefString(node).get(), base, stateValue); ReferenceTreatment treatment = ref.getTreatment(options); switch (treatment) { case INLINE_NONCONFORMING: { - // same behavior as in the LOAD phase walk method + // same behavior as in the LOAD phase visit method // TODO handle cycles Content toInline = contentManager.load(ref, state); toInline.scan(ScanOp.LOAD); - return toInline.isValid() ? Disposition.rewalk(toInline.copyTree()) - : Disposition.done(toInline.getRef().getRefNode(false)); + return toInline.isValid() ? Disposition.revisit().withReplacement(toInline.copyTree()) + : Disposition.done().withReplacement(toInline.getRef().getRefNode(false)); } case INLINE_CONFORMING: { // almost the same, but if this reference creates a cycle, we localize it @@ -204,8 +191,8 @@ public Disposition policyWalkMethod(JsonNode node, State state, E stateValue, if (toInline.isValid()) { toInline.scan(ScanOp.LOAD); } - return toInline.isValid() ? Disposition.rewalk(toInline.copyTree()) - : Disposition.done(toInline.getRef().getRefNode(false)); + return toInline.isValid() ? Disposition.revisit().withReplacement(toInline.copyTree()) + : Disposition.done().withReplacement(toInline.getRef().getRefNode(false)); } case LOCALIZE: { Content toLocalize = contentManager.load(ref, state); @@ -214,9 +201,9 @@ public Disposition policyWalkMethod(JsonNode node, State state, E stateValue, toLocalize.scan(ScanOp.POLICY); LocalizedContent localized = contentManager.localize(toLocalize.getTree(), stateValue, ref.getPointer(), ref); - return Disposition.done(localized.getLocalizedRef(ref).getRefNode(false)); + return Disposition.done().withReplacement(localized.getLocalizedRef(ref).getRefNode(false)); } else { - return Disposition.done(ref.getRefNode(false)); + return Disposition.done().withReplacement(ref.getRefNode(false)); } } case MERGE: { @@ -230,13 +217,13 @@ public Disposition policyWalkMethod(JsonNode node, State state, E stateValue, } else { contentManager.mergeLocalize(ref.getRefNode(false), stateValue, pointer, base); } - return Disposition.done(ref.getRefNode(false)); + return Disposition.done().withReplacement(ref.getRefNode(false)); } case ERROR: case RETAIN: // pass through erroneous and retained references, but remove any adornments // from prior phases - return Disposition.done(ref.getRefNode(false)); + return Disposition.done().withReplacement(ref.getRefNode(false)); default: break; diff --git a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/JsonStateWalker.java b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/JsonStateWalker.java index 516bb86..a4a69d2 100644 --- a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/JsonStateWalker.java +++ b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/JsonStateWalker.java @@ -1,5 +1,9 @@ package com.reprezen.kaizen.normalizer.util; +import static com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.Action.DESCEND; +import static com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.Action.DONE; +import static com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.Action.REVISIT; + import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -17,24 +21,29 @@ * making moves with a supplied tracker whenever descending into child nodes. * * The walker is intialized with a {@link Tracker}, as indicated above, as well - * as a walk method in the form of either an {@link AdvancedWalkMethod} or a - * {@link SimpleWalkMethod}. As the tree is walked (in a depth-first fashion), - * the walk method is invoked for each visited node. + * as a visitn method in the form of either an {@link AdvancedVisitnMethod} or a + * {@link SimpleVisitMethod}. As the tree is walked (in a depth-first fashion), + * the visit method is invoked for each visited node. * - * When walking the children of an object node, the tracker moves to each - * property name before walking the property value, and then backs up - * immediately afterward. When walking the children of an array node, the - * tracker moves to each element index before walking the element, and then + * When visiting the children of an object node, the tracker moves to each + * property name before visiting the property value, and then backs up + * immediately afterward. When visiting the children of an array node, the + * tracker moves to each element index before visiting the element, and then * backs up immediately afterward. * - * An advanced walk method is capable of providing a replacement JsonNode value - * for the node being walked, and it can also control descent into the walked - * node's children. A simple walk method, in contrast, can do neither of these; - * descents always happen, and no replacements occur as a result fo the walk. + * An advanced visit method is capable of: + *
      + *
    • providing a replacement JsonNode value for the node being visited;
    • + *
    • controlling descent into the visited node's children; and
    • + *
    • providing a callback method (void, no-arg) to be invoked just before + * completing this visit.
    • + *
        + * A simple visit method, in contrast, can do none of these; descents always + * happen, and no replacements or callbacks occur as a result of the visit. * - * The walker can optionally suppress invocation of the walk method whenever the - * current state reported by the tracker is anonymous, or when the tracker is - * off-road, or both. + * The walker can optionally suppress invocation of the visit method whenever + * the current state reported by the tracker is anonymous, or when the tracker + * is off-road, or both. * * @author Andy Lowry * @@ -43,8 +52,8 @@ public class JsonStateWalker> { private Tracker tracker; - private AdvancedWalkMethod walkMethod; - private boolean walkAnonymousStates; + private AdvancedVisitMethod visitMethod; + private boolean visitAnonymousStates; private boolean walkOffRoad; /** @@ -52,59 +61,59 @@ public class JsonStateWalker> { * * @param tracker * the tracker to use during the walk - * @param walkMethod - * the walk method to invoke + * @param visitMethod + * the visit method to invoke */ - public JsonStateWalker(Tracker tracker, SimpleWalkMethod walkMethod) { - this(tracker, walkMethod, false, false); + public JsonStateWalker(Tracker tracker, SimpleVisitMethod visitMethod) { + this(tracker, visitMethod, false, false); } /** - * Create a walker with a simple walk method + * Create a walker with a simple visit method * * @param tracker * the tracker to use during the walk - * @param walkMethod - * the walk method to invoke - * @param walkAnonymousStates - * whether to walk nodes corresponding to anonymous machine states + * @param visitMethod + * the visit method to invoke + * @param visitAnonymousStates + * whether to visit nodes corresponding to anonymous machine states * @param walkOffRoad - * whether to walk nodes when the tracker is off-road + * whether to visit nodes when the tracker is off-road */ - public JsonStateWalker(Tracker tracker, SimpleWalkMethod walkMethod, boolean walkAnonymousStates, + public JsonStateWalker(Tracker tracker, SimpleVisitMethod visitMethod, boolean visitAnonymousStates, boolean walkOffRoad) { - this(tracker, walkMethod.asAdvancedWalkMethod(), walkAnonymousStates, walkOffRoad); + this(tracker, visitMethod.asAdvancedVisitMethod(), visitAnonymousStates, walkOffRoad); } /** - * Create a walker with an advanced walk method + * Create a walker with an advanced visit method * * @param tracker * the tracker to use during the walk - * @param walkMethod - * the walk method to invoke + * @param visitMethod + * the visit method to invoke */ - public JsonStateWalker(Tracker tracker, AdvancedWalkMethod walkMethod) { - this(tracker, walkMethod, false, false); + public JsonStateWalker(Tracker tracker, AdvancedVisitMethod visitMethod) { + this(tracker, visitMethod, false, false); } /** - * Create a walker with an advanced walk method + * Create a walker with an advanced visit method * * @param tracker * the tracker to use during the walk - * @param walkMethod - * the walk method to invoke - * @param walkAnonymousStates - * whether to walk nodes corresponding to anonymous machine states + * @param visitMethod + * the visit method to invoke + * @param visitAnonymousStates + * whether to visit nodes corresponding to anonymous machine states * @param walkOffRoad - * whether to walk nodes when the tracker is off-road + * whether to visit nodes when the tracker is off-road */ - public JsonStateWalker(Tracker tracker, AdvancedWalkMethod walkMethod, boolean walkAnonymousStates, + public JsonStateWalker(Tracker tracker, AdvancedVisitMethod visitMethod, boolean visitAnonymousStates, boolean walkOffRoad) { this.tracker = tracker; - this.walkMethod = walkMethod; - this.walkAnonymousStates = walkAnonymousStates; + this.visitMethod = visitMethod; + this.visitAnonymousStates = visitAnonymousStates; this.walkOffRoad = walkOffRoad; } @@ -112,40 +121,48 @@ public JsonStateWalker(Tracker tracker, AdvancedWalkMethod walkMethod, boo * Perform the walk * * @param node - * JsonNode value to walk - * @return optional replacement node. This will be present if the walk method + * root of the JsonNode tree to walk + * @return optional replacement node. This will be present if the visit method * specified a replacement for the provided top-level node. Replacements * of interior nodes are done in-place and will therefore be reflected * in the provided node whether or not that node is replaced. */ public Optional walk(JsonNode node) { + return visit(node); + } + + private Optional visit(JsonNode node) { State state = tracker.getCurrentState(); boolean replaced = false; boolean descend = true; - boolean keepWalking = state != null ? state.getValue() != null ? true : walkAnonymousStates : walkOffRoad; - while (keepWalking) { + boolean keepVisiting = state != null ? (state.isAnonymous() ? visitAnonymousStates : true) : walkOffRoad; + // don't descend into children if we're not visiting this node in the first + // place + Disposition disp = keepVisiting ? Disposition.descend() : Disposition.done(); + while (keepVisiting) { State currentState = tracker.getCurrentState(); - Disposition disp = walkMethod.walk(node, currentState, - currentState != null ? currentState.getValue() : null, tracker.getPath(), getPointer(tracker)); + disp = visitMethod.visit(node, currentState, + currentState != null ? currentState.getValue() : tracker.getOffRoadValue(), tracker.getPath(), + getPointer(tracker)); JsonNode replacement = disp.getReplacement(); boolean replacedThisTime = false; - // don't do a rewalk unless it specifies a replacement node that is different + // don't do a revisit unless it specifies a replacement node that is different // (as in ==, not Object#equals) from the the current node if (replacement != null && replacement != node) { node = replacement; replacedThisTime = replaced = true; } switch (disp.getAction()) { - case Disposition.DESCEND: - keepWalking = false; + case DESCEND: + keepVisiting = false; break; - case Disposition.REWALK: + case REVISIT: if (!replacedThisTime) { - keepWalking = false; + keepVisiting = false; } break; - case Disposition.DONE: - keepWalking = false; + case DONE: + keepVisiting = false; descend = false; break; } @@ -157,6 +174,9 @@ public Optional walk(JsonNode node) { walkArray((ArrayNode) node); } } + if (disp.getCallback() != null) { + disp.getCallback().call(); + } return replaced ? Optional.of(node) : Optional.empty(); } @@ -164,7 +184,7 @@ private void walkObject(ObjectNode node) { for (Iterator iter = node.fieldNames(); iter.hasNext();) { String name = iter.next(); tracker.move(name); - Optional replacement = walk(node.get(name)); + Optional replacement = visit(node.get(name)); if (replacement.isPresent()) { node.set(name, replacement.get()); } @@ -175,7 +195,7 @@ private void walkObject(ObjectNode node) { private void walkArray(ArrayNode node) { for (int i = 0; i < node.size(); i++) { tracker.move(i); - Optional replacement = walk(node.get(i)); + Optional replacement = visit(node.get(i)); if (replacement.isPresent()) { node.set(i, replacement.get()); } @@ -192,9 +212,9 @@ private JsonPointer getPointer(Tracker tracker) { } @FunctionalInterface - public interface AdvancedWalkMethod> { + public interface AdvancedVisitMethod> { /** - * Process a JsonNode value during a state walk + * Process a JsonNode value during a state visit * * @param node * The node to process @@ -212,13 +232,13 @@ public interface AdvancedWalkMethod> { * @return disposition information, including how to proceed and an optional * replacement for this node */ - Disposition walk(JsonNode node, State state, E stateValue, List path, JsonPointer pointer); + Disposition visit(JsonNode node, State state, E stateValue, List path, JsonPointer pointer); } @FunctionalInterface - public interface SimpleWalkMethod> { + public interface SimpleVisitMethod> { /** - * Like {@link AdvancedWalkMethod} but with no return value; normal disposition + * Like {@link AdvancedVisitMethod} but with no return value; normal disposition * is always used. * * @param node @@ -235,20 +255,20 @@ public interface SimpleWalkMethod> { * JsonPointer specifying location of node in overall walked tree * (constructed from tracker path) */ - void walk(JsonNode node, State state, E stateValue, List path, JsonPointer pointer); + void visit(JsonNode node, State state, E stateValue, List path, JsonPointer pointer); /** - * Method to deliver an advanced walk method that is equivalent to this simple - * walk method + * Method to deliver an advanced visit method that is equivalent to this simple + * visit method * - * @return equivalent {@link AdvancedWalkMethod} + * @return equivalent {@link AdvancedVisitMethod} */ - default AdvancedWalkMethod asAdvancedWalkMethod() { - return new AdvancedWalkMethod() { + default AdvancedVisitMethod asAdvancedVisitMethod() { + return new AdvancedVisitMethod() { @Override - public Disposition walk(JsonNode node, State state, E stateValue, List path, + public Disposition visit(JsonNode node, State state, E stateValue, List path, JsonPointer pointer) { - SimpleWalkMethod.this.walk(node, state, stateValue, path, pointer); + SimpleVisitMethod.this.visit(node, state, stateValue, path, pointer); return Disposition.normal(); } }; @@ -256,47 +276,43 @@ public Disposition walk(JsonNode node, State state, E stateValue, List - *
      • Provide a replacement value for the walked node
      • + *
      • Provide a replacement value for the visited node
      • *
      • Specify one of three behaviors wrt descending into child nodes
      • *
        *
        DESCEND
        - *
        Walk child nodes of the walked node (not of the replacment, if any)
        + *
        Visit child nodes of the visited node (not of the replacment, if + * any)
        *
        REWALK
        - *
        Walk the replacement node without descending into its children, and then + *
        Visit the replacement node without descending into its children, and then * proceed according to the resulting disposition
        *
        DONE
        - *
        Do not walk the child nodes of the walked node
        + *
        Do not visit the child nodes of the visited node
        *
        * * Note that REWALK is effective ONLY when a replacement node is - * provided, and that replacement is not the same as the walked node. Re-walking - * these conditions are not met is guaranteed to result in an infinite loop, - * assuming a stateless walk method. Absent these conditions, REWALK is - * treated exactly like DESCEND. + * provided, and that replacement is not the same as the visited node. + * Re-visiting these conditions are not met is guaranteed to result in an + * infinite loop, assuming a stateless visit method. Absent these conditions, + * REWALK is treated exactly like DESCEND. * * @author Andy Lowry * */ public static class Disposition { - public static final String DESCEND = "descend"; - public static final String REWALK = "rewalk"; - public static final String DONE = "done"; - - private static final Disposition descendDisposition = descend(null); - private static final Disposition rewalkDisposition = rewalk(null); - private static final Disposition doneDisposition = done(null); - private static final Disposition normalDisposition = descendDisposition; + public static enum Action { + DESCEND, REVISIT, DONE + } - private String action; + private Action action; private JsonNode replacement; + private CallbackMethod callback; - private Disposition(String action, JsonNode replacement) { + private Disposition(Action action) { this.action = action; - this.replacement = replacement; } /** @@ -306,39 +322,46 @@ private Disposition(String action, JsonNode replacement) { * @return */ public static Disposition normal() { - return normalDisposition; + return descend(); } public static Disposition descend() { - return descendDisposition; - } - - public static Disposition descend(JsonNode replacement) { - return new Disposition(DESCEND, replacement); + return new Disposition(DESCEND); } - public static Disposition rewalk() { - return rewalkDisposition; + public static Disposition revisit() { + return new Disposition(REVISIT); } - public static Disposition rewalk(JsonNode replacement) { - return new Disposition(REWALK, replacement); + public static Disposition done() { + return new Disposition(DONE); } - public static Disposition done() { - return doneDisposition; + public Disposition withReplacement(JsonNode replacement) { + this.replacement = replacement; + return this; } - public static Disposition done(JsonNode replacement) { - return new Disposition(DONE, replacement); + public Disposition withCallback(CallbackMethod callback) { + this.callback = callback; + return this; } - public String getAction() { + public Action getAction() { return action; } public JsonNode getReplacement() { return replacement; } + + public CallbackMethod getCallback() { + return callback; + } + } + + @FunctionalInterface + public interface CallbackMethod { + void call(); } } diff --git a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java index c8e3647..6281ac5 100644 --- a/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java +++ b/kaizen-openapi-normalizer/src/main/java/com/reprezen/kaizen/normalizer/util/StateMachine.java @@ -243,7 +243,7 @@ private State installEdge(State start, String label, State end) { } // no existing edge with identical label... create new edge, either to provided // end state or to a new anonymous state - State target = end != null ? end : new State(anonymousValue); + State target = end != null ? end : new State(anonymousValue, true); existingEdges.add(new Edge(label, target)); return target; } @@ -288,7 +288,7 @@ public void copyOutEdges(E from, E to) { */ public State getState(E name) { if (!namedStates.containsKey(name)) { - State state = new State(name); + State state = new State(name, false); namedStates.put(name, state); } return namedStates.get(name); @@ -605,7 +605,7 @@ public State move(int value) { private State moveTo(State newState, Object edgeValue) { if (newState == null && offRoadValue != null) { - newState = new State(offRoadValue); + newState = new State(offRoadValue, false); } crumbs.add(currentState); path.add(edgeValue); @@ -718,6 +718,16 @@ public void reset() { public List getPath() { return new ArrayList<>(path); } + + /** + * Return the value used for off-road states, if one has been provided for the + * machine + * + * @return off-road value, or null if none has been provided + */ + public E getOffRoadValue() { + return offRoadValue; + } } /** @@ -736,15 +746,20 @@ public List getPath() { */ public static class State> { private E value = null; + private boolean anonymous; /** * Create a new state for the given enum value * - * @param name - * enum value, or null for anonymous or off-road state + * @param value + * enum value, or null for an off-road state; may also be null for an + * anonymous state + * @param anonymous + * whether this is an anonymous state */ - private State(E name) { - this.value = name; + private State(E value, boolean anonymous) { + this.value = value; + this.anonymous = anonymous; } /** @@ -757,6 +772,10 @@ public E getValue() { return value; } + public boolean isAnonymous() { + return anonymous; + } + @Override public String toString() { return String.format("State[%s]", getValue()); diff --git a/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/JsonStateWalkerTest.java b/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/JsonStateWalkerTest.java index cc3199a..e06442c 100644 --- a/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/JsonStateWalkerTest.java +++ b/kaizen-openapi-normalizer/src/test/java/com/reprezen/kaizen/normalizer/test/JsonStateWalkerTest.java @@ -1,8 +1,9 @@ package com.reprezen.kaizen.normalizer.test; -import static com.reprezen.kaizen.normalizer.test.JsonStateWalkerTest.WalkResult.walkResult; -import static com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.DONE; -import static com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.REWALK; +import static com.reprezen.kaizen.normalizer.test.JsonStateWalkerTest.VisitResult.visitResult; +import static com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.Action.DESCEND; +import static com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.Action.DONE; +import static com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.Action.REVISIT; import java.io.IOException; import java.util.ArrayList; @@ -21,9 +22,10 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.TextNode; import com.reprezen.kaizen.normalizer.util.JsonStateWalker; -import com.reprezen.kaizen.normalizer.util.JsonStateWalker.AdvancedWalkMethod; +import com.reprezen.kaizen.normalizer.util.JsonStateWalker.AdvancedVisitMethod; import com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition; -import com.reprezen.kaizen.normalizer.util.JsonStateWalker.SimpleWalkMethod; +import com.reprezen.kaizen.normalizer.util.JsonStateWalker.Disposition.Action; +import com.reprezen.kaizen.normalizer.util.JsonStateWalker.SimpleVisitMethod; import com.reprezen.kaizen.normalizer.util.StateMachine; import com.reprezen.kaizen.normalizer.util.StateMachine.State; import com.reprezen.kaizen.normalizer.util.StateMachine.Tracker; @@ -47,62 +49,62 @@ public void setup() throws JsonProcessingException, IOException { @Test public void simpleTest() { - SimpleWalkMethod walk = (n, s, v, path, ptr) -> { + SimpleVisitMethod visitMehod = (n, s, v, path, ptr) -> { }; - verifyWalk(tree, machine.tracker(S.TOP), walk, // - walkResult(S.TOP, ""), // - walkResult(S.A, "/a"), // - walkResult(S.A, "/a/a"), // - walkResult(S.B, "/a/a/b"), // - walkResult(S.C, "/a/a/b/0"), // - walkResult(S.C, "/a/a/b/1"), // - walkResult(S.C, "/a/a/b/2"), // - walkResult(S.C, "/a/a/b/3"), // - walkResult(S.OFF, "/a/a/b/3/0"), // - walkResult(S.OFF, "/a/a/b/3/1"), // - walkResult(S.OFF, "/a/a/b/3/2"), // - walkResult(S.B, "/a/b"), // - walkResult(S.C, "/a/b/0"), // - walkResult(S.C, "/c")); + verifyWalk(tree, machine.tracker(S.TOP), visitMehod, // + visitResult(S.TOP, ""), // + visitResult(S.A, "/a"), // + visitResult(S.A, "/a/a"), // + visitResult(S.B, "/a/a/b"), // + visitResult(S.C, "/a/a/b/0"), // + visitResult(S.C, "/a/a/b/1"), // + visitResult(S.C, "/a/a/b/2"), // + visitResult(S.C, "/a/a/b/3"), // + visitResult(S.OFF, "/a/a/b/3/0"), // + visitResult(S.OFF, "/a/a/b/3/1"), // + visitResult(S.OFF, "/a/a/b/3/2"), // + visitResult(S.B, "/a/b"), // + visitResult(S.C, "/a/b/0"), // + visitResult(S.C, "/c")); } @Test public void pruneTest() { - AdvancedWalkMethod walk = (n, s, v, path, ptr) -> { + AdvancedVisitMethod visitMethod = (n, s, v, path, ptr) -> { return v == S.C ? Disposition.done() : Disposition.normal(); }; - verifyWalk(tree, machine.tracker(S.TOP), walk, // - walkResult(S.TOP, ""), // - walkResult(S.A, "/a"), // - walkResult(S.A, "/a/a"), // - walkResult(S.B, "/a/a/b"), // - walkResult(S.C, "/a/a/b/0", DONE), // - walkResult(S.C, "/a/a/b/1", DONE), // - walkResult(S.C, "/a/a/b/2", DONE), // - walkResult(S.C, "/a/a/b/3", DONE), // - walkResult(S.B, "/a/b"), // - walkResult(S.C, "/a/b/0", DONE), // - walkResult(S.C, "/c", DONE)); + verifyWalk(tree, machine.tracker(S.TOP), visitMethod, // + visitResult(S.TOP, ""), // + visitResult(S.A, "/a"), // + visitResult(S.A, "/a/a"), // + visitResult(S.B, "/a/a/b"), // + visitResult(S.C, "/a/a/b/0", DONE), // + visitResult(S.C, "/a/a/b/1", DONE), // + visitResult(S.C, "/a/a/b/2", DONE), // + visitResult(S.C, "/a/a/b/3", DONE), // + visitResult(S.B, "/a/b"), // + visitResult(S.C, "/a/b/0", DONE), // + visitResult(S.C, "/c", DONE)); } @Test public void replaceTest() { - AdvancedWalkMethod walk = (n, s, v, path, ptr) -> { - return v == S.C ? Disposition.done(TextNode.valueOf("replaced")) : Disposition.normal(); + AdvancedVisitMethod walk = (n, s, v, path, ptr) -> { + return v == S.C ? Disposition.done().withReplacement(TextNode.valueOf("replaced")) : Disposition.normal(); }; JsonNode newTree = verifyWalk(tree, machine.tracker(S.TOP), walk, // - walkResult(S.TOP, ""), // - walkResult(S.A, "/a"), // - walkResult(S.A, "/a/a"), // - walkResult(S.B, "/a/a/b"), // - walkResult(S.C, "/a/a/b/0", DONE), // - walkResult(S.C, "/a/a/b/1", DONE), // - walkResult(S.C, "/a/a/b/2", DONE), // - walkResult(S.C, "/a/a/b/3", DONE), // - walkResult(S.B, "/a/b"), // - walkResult(S.C, "/a/b/0", DONE), // - walkResult(S.C, "/c", DONE)); - SimpleWalkMethod checkReplacements = (n, s, v, path, ptr) -> { + visitResult(S.TOP, ""), // + visitResult(S.A, "/a"), // + visitResult(S.A, "/a/a"), // + visitResult(S.B, "/a/a/b"), // + visitResult(S.C, "/a/a/b/0", DONE), // + visitResult(S.C, "/a/a/b/1", DONE), // + visitResult(S.C, "/a/a/b/2", DONE), // + visitResult(S.C, "/a/a/b/3", DONE), // + visitResult(S.B, "/a/b"), // + visitResult(S.C, "/a/b/0", DONE), // + visitResult(S.C, "/c", DONE)); + SimpleVisitMethod checkReplacements = (n, s, v, path, ptr) -> { if (v == S.C) { assertEquals("Replacement failed", TextNode.valueOf("replaced"), n); } @@ -111,29 +113,26 @@ public void replaceTest() { } @Test - public void rewalkTest() { - AdvancedWalkMethod walk = (n, s, v, path, ptr) -> { + public void revisitTest() { + AdvancedVisitMethod visit = (n, s, v, path, ptr) -> { if (v == S.B) { JsonNode replacement = arrayNode(TextNode.valueOf("x")); - return Disposition.rewalk(n.equals(replacement) ? null : replacement); - // if (!n.equals(replacement)) { - // return Disposition.rewalk(replacement); - // } + return Disposition.revisit().withReplacement(n.equals(replacement) ? null : replacement); } return Disposition.normal(); }; - JsonNode newTree = verifyWalk(tree, machine.tracker(S.TOP), walk, // - walkResult(S.TOP, ""), // - walkResult(S.A, "/a"), // - walkResult(S.A, "/a/a"), // - walkResult(S.B, "/a/a/b", REWALK), // - walkResult(S.B, "/a/a/b", REWALK), // - walkResult(S.C, "/a/a/b/0"), // - walkResult(S.B, "/a/b", REWALK), // - walkResult(S.B, "/a/b", REWALK), // - walkResult(S.C, "/a/b/0"), // - walkResult(S.C, "/c")); - SimpleWalkMethod checkReplacements = (n, s, v, path, ptr) -> { + JsonNode newTree = verifyWalk(tree, machine.tracker(S.TOP), visit, // + visitResult(S.TOP, ""), // + visitResult(S.A, "/a"), // + visitResult(S.A, "/a/a"), // + visitResult(S.B, "/a/a/b", REVISIT), // + visitResult(S.B, "/a/a/b", REVISIT), // + visitResult(S.C, "/a/a/b/0"), // + visitResult(S.B, "/a/b", REVISIT), // + visitResult(S.B, "/a/b", REVISIT), // + visitResult(S.C, "/a/b/0"), // + visitResult(S.C, "/c")); + SimpleVisitMethod checkReplacements = (n, s, v, path, ptr) -> { if (v == S.B) { assertEquals("Replacement failed at " + ptr, arrayNode(TextNode.valueOf("x")), n); } @@ -148,20 +147,20 @@ private ArrayNode arrayNode(JsonNode... elements) { return array; } - private > void verifyWalk(JsonNode tree, Tracker tracker, SimpleWalkMethod walkMethod, - WalkResult... expected) { - verifyWalk(tree, tracker, walkMethod.asAdvancedWalkMethod(), expected); + private > void verifyWalk(JsonNode tree, Tracker tracker, SimpleVisitMethod walkMethod, + VisitResult... expected) { + verifyWalk(tree, tracker, walkMethod.asAdvancedVisitMethod(), expected); } - private > JsonNode verifyWalk(JsonNode tree, Tracker tracker, AdvancedWalkMethod walkMethod, - WalkResult... expected) { - List> results = new ArrayList<>(); - AdvancedWalkMethod wrappedWalkMethod = new AdvancedWalkMethod() { + private > JsonNode verifyWalk(JsonNode tree, Tracker tracker, + AdvancedVisitMethod visitMethod, VisitResult... expected) { + List> results = new ArrayList<>(); + AdvancedVisitMethod wrappedWalkMethod = new AdvancedVisitMethod() { @Override - public Disposition walk(JsonNode node, State state, E stateValue, List path, + public Disposition visit(JsonNode node, State state, E stateValue, List path, JsonPointer pointer) { - Disposition disp = walkMethod.walk(node, state, stateValue, path, pointer); - results.add(walkResult(stateValue, pointer.toString(), disp.getAction())); + Disposition disp = visitMethod.visit(node, state, stateValue, path, pointer); + results.add(visitResult(stateValue, pointer.toString(), disp.getAction())); return disp; } }; @@ -170,7 +169,7 @@ public Disposition walk(JsonNode node, State state, E stateValue, List> void checkWalkResults(List> expected, List> actual) { + private > void checkWalkResults(List> expected, List> actual) { for (int i = 0; i < expected.size() && i < actual.size(); i++) { assertEquals("Walk result #" + i + " is incorrect", expected.get(i), actual.get(i)); } @@ -182,23 +181,23 @@ private > void checkWalkResults(List> expected, } } - public static class WalkResult> { + public static class VisitResult> { E stateValue; JsonPointer pointer; - String action; + Action action; - private WalkResult(E stateValue, JsonPointer pointer, String action) { + private VisitResult(E stateValue, JsonPointer pointer, Action action) { this.stateValue = stateValue; this.pointer = pointer; this.action = action; } - public static > WalkResult walkResult(E stateValue, String pointer, String action) { - return new WalkResult(stateValue, JsonPointer.compile(pointer), action); + public static > VisitResult visitResult(E stateValue, String pointer, Action action) { + return new VisitResult(stateValue, JsonPointer.compile(pointer), action); } - public static > WalkResult walkResult(E stateValue, String pointer) { - return walkResult(stateValue, pointer, Disposition.DESCEND); + public static > VisitResult visitResult(E stateValue, String pointer) { + return visitResult(stateValue, pointer, DESCEND); } @Override @@ -224,7 +223,7 @@ public boolean equals(Object obj) { return false; if (getClass() != obj.getClass()) return false; - WalkResult other = (WalkResult) obj; + VisitResult other = (VisitResult) obj; if (action == null) { if (other.action != null) return false;