diff --git a/json-smart/src/main/java/net/minidev/json/actions/ElementRemover.java b/json-smart/src/main/java/net/minidev/json/actions/ElementRemover.java
new file mode 100755
index 00000000..c501b58e
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/ElementRemover.java
@@ -0,0 +1,50 @@
+package net.minidev.json.actions;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.traverse.JSONTraverser;
+import net.minidev.json.actions.traverse.RemoveElementsJsonAction;
+import net.minidev.json.actions.traverse.JSONTraverseAction;
+
+import java.util.*;
+
+/**
+ * Removes key:value elements from every node of a {@link JSONObject} matching the list of user-specified elements.
+ *
+ * An element to remove must be specified as a key:value pair
+ *
+ * Usage Example:
+ *
+ * To remove the element k2:v2 from the {@link JSONObject} {k0:{k2:v2, k3:v3}, k1:{k2:v2, k4:v4}} use the remover like so:
+ *
+ * PathRemover pr = new PathRemover("k2.v2");
+ * JSONObject cleanObject = pr.remove(new JSONObject(...));
+ *
+ * The resulting object 'cleanObject' would be {k0:{k3:v3}, k1:{k4:v4}}
+ *
+ * See unit tests for more examples
+ *
+ * @author adoneitan@gmail.com
+ *
+ */
+public class ElementRemover
+{
+ private Map elementsToRemove;
+
+ public ElementRemover(Map elementsToRemove)
+ {
+ this.elementsToRemove = elementsToRemove == null ? Collections.emptyMap() : elementsToRemove;
+ }
+
+ public ElementRemover(JSONObject elementsToRemove)
+ {
+ this.elementsToRemove = elementsToRemove == null ? Collections.emptyMap() : elementsToRemove;
+ }
+
+ public JSONObject remove(JSONObject objectToClean)
+ {
+ JSONTraverseAction strategy = new RemoveElementsJsonAction(this.elementsToRemove);
+ JSONTraverser traversal = new JSONTraverser(strategy);
+ traversal.traverse(objectToClean);
+ return (JSONObject) strategy.result();
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/PathLocator.java b/json-smart/src/main/java/net/minidev/json/actions/PathLocator.java
new file mode 100755
index 00000000..e75d5736
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/PathLocator.java
@@ -0,0 +1,74 @@
+package net.minidev.json.actions;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.path.DotDelimiter;
+import net.minidev.json.actions.path.PathDelimiter;
+import net.minidev.json.actions.traverse.JSONTraverser;
+import net.minidev.json.actions.traverse.LocatePathsJsonAction;
+import net.minidev.json.actions.traverse.JSONTraverseAction;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Searches for paths in a {@link JSONObject} and returns those found.
+ *
+ * Traverses the specified {@link JSONObject} searching for nodes whose paths (from the root down) match
+ * any of the user-specified paths. The paths that match are returned.
+ *
+ * A path to locate must be specified in the n-gram format - a list of keys from the root down separated by dots:
+ * K0[[[[.K1].K2].K3]...]
+ *
+ * A key to the right of a dot is a direct child of a key to the left of a dot. Keys with a dot in their name are
+ * not supported.
+ *
+ *
+ * @author adoneitan@gmail.com
+ */
+public class PathLocator
+{
+ protected List pathsToFind;
+ protected PathDelimiter pathDelimiter = new DotDelimiter().withAcceptDelimiterInNodeName(false);
+
+ public PathLocator(JSONArray pathsToFind)
+ {
+ if (pathsToFind == null || pathsToFind.isEmpty()) {
+ this.pathsToFind = Collections.emptyList();
+ }
+ else
+ {
+ this.pathsToFind = new ArrayList();
+ for (Object s : pathsToFind) {
+ this.pathsToFind.add((String) s);
+ }
+ }
+ }
+
+ public PathLocator(List pathsToFind)
+ {
+ this.pathsToFind = pathsToFind == null || pathsToFind.size() == 0 ?
+ Collections.emptyList() : pathsToFind;
+ }
+
+ public PathLocator(String... pathsToFind)
+ {
+ this.pathsToFind = pathsToFind == null || pathsToFind.length == 0 ?
+ Collections.emptyList() : Arrays.asList(pathsToFind);
+ }
+
+ public PathLocator with(PathDelimiter pathDelimiter) {
+ this.pathDelimiter = pathDelimiter;
+ return this;
+ }
+
+ public List locate(JSONObject object)
+ {
+ JSONTraverseAction action = new LocatePathsJsonAction(this.pathsToFind, pathDelimiter);
+ JSONTraverser traversal = new JSONTraverser(action).with(pathDelimiter);
+ traversal.traverse(object);
+ return (List) action.result();
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/PathRemover.java b/json-smart/src/main/java/net/minidev/json/actions/PathRemover.java
new file mode 100755
index 00000000..c8c4db37
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/PathRemover.java
@@ -0,0 +1,74 @@
+package net.minidev.json.actions;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.traverse.JSONTraverser;
+import net.minidev.json.actions.traverse.RemovePathsJsonAction;
+import net.minidev.json.actions.traverse.JSONTraverseAction;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Removes branches of nodes from a {@link JSONObject} matching the list of user-specified paths.
+ *
+ * A path to remove must be specified in the n-gram format - a list of keys from the root down separated by dots:
+ * K0[[[[.K1].K2].K3]...]
+ *
+ * A key to the right of a dot is a direct child of a key to the left of a dot. Keys with a dot in their name are
+ * not supported.
+ *
+ * Usage Example:
+ *
+ * To remove the field k1.k2 from the {@link JSONObject} {k1:{k2:v2}, k3:{k4:v4}} use the remover like so:
+ *
+ * PathRemover pr = new PathRemover("k1.k2");
+ * JSONObject cleanObject = pr.remove(new JSONObject(...));
+ *
+ * The resulting object 'cleanObject' would be {k1:{k3:{k4:v4}}}
+ *
+ * See unit tests for more examples
+ *
+ * @author adoneitan@gmail.com
+ *
+ */
+public class PathRemover
+{
+ protected List pathsToRemove;
+
+ public PathRemover(JSONArray pathsToRemove)
+ {
+ if (pathsToRemove == null || pathsToRemove.isEmpty()) {
+ this.pathsToRemove = Collections.emptyList();
+ }
+ else
+ {
+ this.pathsToRemove = new ArrayList();
+ for (Object s : pathsToRemove) {
+ this.pathsToRemove.add((String) s);
+ }
+ }
+ }
+
+ public PathRemover(List pathsToRemove)
+ {
+ this.pathsToRemove = pathsToRemove == null || pathsToRemove.size() == 0 ?
+ Collections.emptyList() : pathsToRemove;
+ }
+
+ public PathRemover(String... pathsToRemove)
+ {
+ this.pathsToRemove = pathsToRemove == null || pathsToRemove.length == 0 ?
+ Collections.emptyList() : Arrays.asList(pathsToRemove);
+ }
+
+ public JSONObject remove(JSONObject objectToClean)
+ {
+ JSONTraverseAction strategy = new RemovePathsJsonAction(this.pathsToRemove);
+ JSONTraverser traversal = new JSONTraverser(strategy);
+ traversal.traverse(objectToClean);
+ return (JSONObject) strategy.result();
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/PathReplicator.java b/json-smart/src/main/java/net/minidev/json/actions/PathReplicator.java
new file mode 100755
index 00000000..65173a35
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/PathReplicator.java
@@ -0,0 +1,76 @@
+package net.minidev.json.actions;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.navigate.CopyPathsAction;
+import net.minidev.json.actions.navigate.JSONNavigator;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Creates a copy of a {@link JSONObject} consisting only of the nodes on the user-specified paths.
+ *
+ * Paths that do not exist in the specified object are ignored silently. Specifying an empty list of paths
+ * to copy or only non-existent paths will result in an empty object being returned.
+ *
+ * A path to copy must be specified in the n-gram format - a list of keys from the root down separated by dots:
+ * K0[[[[.K1].K2].K3]...]
+ *
+ * A key to the right of a dot is a direct child of a key to the left of a dot. Keys with a dot in their name are
+ * not supported.
+ *
+ * Sample usage:
+ *
+ * To replicate the branch k1.k2 from {k1:{k2:v2}, k3:{k4:v4}} use the {@link PathReplicator} like so:
+ *
+ * PathReplicator pr = new {@link PathReplicator}("k1.k2")
+ * JSONObject copiedObject = pr.copy(new JSONObject(...))
+ *
+ * The resulting object 'copiedObject' would be {k1:{k2:v2}}
+ *
+ * see unit tests for more examples
+ *
+ * @author adoneitan@gmail.com
+ * @since 15 March 2016.
+ */
+public class PathReplicator
+{
+ protected List pathsToCopy;
+
+ public PathReplicator(JSONArray pathsToCopy)
+ {
+ if (pathsToCopy == null || pathsToCopy.isEmpty()) {
+ this.pathsToCopy = Collections.emptyList();
+ }
+ else
+ {
+ this.pathsToCopy = new LinkedList();
+ for (Object s : pathsToCopy) {
+ this.pathsToCopy.add((String) s);
+ }
+ }
+ }
+
+ public PathReplicator(List pathsToCopy)
+ {
+ this.pathsToCopy = pathsToCopy == null || pathsToCopy.size() == 0 ?
+ Collections.emptyList() : pathsToCopy;
+ }
+
+ public PathReplicator(String... pathsToCopy)
+ {
+ this.pathsToCopy = pathsToCopy == null || pathsToCopy.length == 0 ?
+ Collections.emptyList() : new LinkedList(Arrays.asList(pathsToCopy));
+ }
+
+ public JSONObject replicate(JSONObject sourceObj) throws Exception
+ {
+ CopyPathsAction s = new CopyPathsAction();
+ JSONNavigator n = new JSONNavigator(s, pathsToCopy);
+ n.nav(sourceObj);
+ return (JSONObject) s.result();
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/PathsRetainer.java b/json-smart/src/main/java/net/minidev/json/actions/PathsRetainer.java
new file mode 100755
index 00000000..f4fa2496
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/PathsRetainer.java
@@ -0,0 +1,93 @@
+package net.minidev.json.actions;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.path.DotDelimiter;
+import net.minidev.json.actions.path.PathDelimiter;
+import net.minidev.json.actions.traverse.JSONTraverseAction;
+import net.minidev.json.actions.traverse.JSONTraverser;
+import net.minidev.json.actions.traverse.LocatePathsJsonAction;
+import net.minidev.json.actions.traverse.RetainPathsJsonAction;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Retains branches of nodes of a {@link JSONObject} matching the list of user-specified paths.
+ *
+ * A path to copy must be specified in the n-gram format - a list of keys from the root down separated by dots:
+ * K0[[[[.K1].K2].K3]...]
+ *
+ * A key to the right of a dot is a direct child of a key to the left of a dot. Keys with a dot in their name are
+ * not supported.
+ *
+ * Example:
+ *
+ * to retain the field k1.k2 in the {@link JSONObject} {k1:{k2:v1}, k3:{k4:v2}} instantiate the retainer like so:
+ * new JSONObjectCleaner("k1.k2")
+ * The resulting object would be {k1:{k2:v1}}
+ *
+ * See unit tests in JSONObjectRetainerTest for more examples
+ *
+ * @author adoneitan@gmail.com
+ */
+public class PathsRetainer
+{
+ protected List pathsToRetain;
+ protected PathDelimiter pathDelimiter = new DotDelimiter().withAcceptDelimiterInNodeName(false);
+
+ public PathsRetainer(JSONArray pathsToRetain)
+ {
+ if (pathsToRetain == null || pathsToRetain.isEmpty()) {
+ this.pathsToRetain = Collections.emptyList();
+ }
+ else
+ {
+ this.pathsToRetain = new LinkedList();
+ for (Object s : pathsToRetain) {
+ this.pathsToRetain.add((String) s);
+ }
+ }
+ }
+
+ public PathsRetainer(List pathsToRetain)
+ {
+ this.pathsToRetain = pathsToRetain == null || pathsToRetain.size() == 0 ?
+ Collections.emptyList() : pathsToRetain;
+ }
+
+ public PathsRetainer(String... pathsToRetain)
+ {
+ this.pathsToRetain = pathsToRetain == null || pathsToRetain.length == 0 ?
+ Collections.emptyList() : new LinkedList(Arrays.asList(pathsToRetain));
+ }
+
+ public PathsRetainer with(PathDelimiter pathDelimiter) {
+ this.pathDelimiter = pathDelimiter;
+ return this;
+ }
+
+ public JSONObject retain(JSONObject object)
+ {
+ /**
+ * a path to retain which contains a path in the object, but is not itself a path in the object,
+ * will cause the sub-path to be retained although it shouldn't:
+ * object = {k0:v0} retain = {k0.k1}
+ * so the false path to retain has to be removed from the pathsToRetain list.
+ *
+ * The {@link LocatePathsJsonAction} returns only paths which exist in the object.
+ */
+ JSONTraverseAction locateAction = new LocatePathsJsonAction(pathsToRetain, pathDelimiter);
+ JSONTraverser t1 = new JSONTraverser(locateAction);
+ t1.traverse(object);
+ List realPathsToRetain = (List) locateAction.result();
+
+ //now reduce the object using only existing paths
+ JSONTraverseAction retainer = new RetainPathsJsonAction(realPathsToRetain, pathDelimiter);
+ JSONTraverser t2 = new JSONTraverser(retainer).with(pathDelimiter);
+ t2.traverse(object);
+ return (JSONObject) retainer.result();
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/navigate/CopyPathsAction.java b/json-smart/src/main/java/net/minidev/json/actions/navigate/CopyPathsAction.java
new file mode 100755
index 00000000..52d5df7e
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/navigate/CopyPathsAction.java
@@ -0,0 +1,145 @@
+package net.minidev.json.actions.navigate;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.path.TreePath;
+
+import java.util.Collection;
+import java.util.Stack;
+
+/**
+ * Creates a copy of a {@link JSONObject} containing just the nodes on the paths specified.
+ *
+ * Specified paths that do not exist in the source object are ignored silently.
+ * Specifying an empty list of paths to navigate or only non-existent paths will result in an empty
+ * object being returned.
+ *
+ * See package-info for more details
+ *
+ * Example:
+ *
+ * To copy the branch k1.k2 from {k1:{k2:v1}, k3:{k4:v2}} instantiate the copier like so:
+ * new JSONObjectCopier("k1.k2")
+ * The resulting copy would be {k1:{k2:v1}}
+ *
+ * See unit tests for more examples
+ *
+ * @author adoneitan@gmail.com
+ * @since 15 March 2016.
+ *
+ */
+public class CopyPathsAction implements JSONNavigateAction
+{
+ protected JSONObject destTree;
+ protected JSONObject destBranch;
+ protected Stack destNodeStack;
+
+ @Override
+ public boolean start(JSONObject source, Collection pathsToCopy)
+ {
+ if (source == null)
+ {
+ destTree = null;
+ return false;
+ }
+ destTree = new JSONObject();
+ if (pathsToCopy == null || pathsToCopy.size() == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(TreePath jp, JSONObject o)
+ {
+ //reached JSONObject node - instantiate it and recur
+ handleNewNode(jp, new JSONObject());
+ return true;
+ }
+
+ private void handleNewNode(TreePath jp, Object node)
+ {
+ if (!jp.hasPrev()) {
+ return;
+ }
+ if (destNodeStack.peek() instanceof JSONObject) {
+ ((JSONObject) destNodeStack.peek()).put(jp.curr(), node);
+ }
+ else if (destNodeStack.peek() instanceof JSONArray) {
+ ((JSONArray) destNodeStack.peek()).add(node);
+ }
+ destNodeStack.push(node);
+ }
+
+ @Override
+ public boolean recurInto(TreePath jp, JSONArray o)
+ {
+ //reached JSONArray node - instantiate it and recur
+ handleNewNode(jp, new JSONArray());
+ return true;
+ }
+
+ @Override
+ public void foundLeafBeforePathEnd(TreePath jp, Object obj) {
+ throw new IllegalArgumentException("branch is shorter than path - path not found in source: '" + jp.origin() + "'");
+ }
+
+ @Override
+ public void pathTailNotFound(TreePath jp, Object source) {
+ throw new IllegalArgumentException("cannot find next element of path - path not found in source: '" + jp.origin() + "'");
+ }
+
+ @Override
+ public void handleLeaf(TreePath jp, Object o) {
+ ((JSONObject) destNodeStack.peek()).put(jp.curr(), o);
+ }
+
+ @Override
+ public void handleLeaf(TreePath jp, int arrIndex, Object o) {
+ ((JSONArray) destNodeStack.peek()).add(o);
+ }
+
+ @Override
+ public void recurEnd(TreePath jp, JSONObject jo) {
+ destNodeStack.pop();
+ }
+
+ @Override
+ public void recurEnd(TreePath jp, JSONArray ja) {
+ destNodeStack.pop();
+ }
+
+ @Override
+ public boolean pathStart(String path)
+ {
+ destBranch = new JSONObject();
+ destNodeStack = new Stack();
+ destNodeStack.push(destBranch);
+ return true;
+ }
+
+ @Override
+ public void pathEnd(String path) {
+ destTree.merge(destBranch);
+ }
+
+ @Override
+ public boolean failSilently(String path, Exception e) {
+ return false;
+ }
+
+ @Override
+ public boolean failFast(String path, Exception e) {
+ return false;
+ }
+
+ @Override
+ public void end() {
+
+ }
+
+ @Override
+ public Object result() {
+ return destTree;
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/navigate/JSONNavigateAction.java b/json-smart/src/main/java/net/minidev/json/actions/navigate/JSONNavigateAction.java
new file mode 100755
index 00000000..8c1adb8a
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/navigate/JSONNavigateAction.java
@@ -0,0 +1,17 @@
+package net.minidev.json.actions.navigate;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+
+/**
+ * An interface for a processing action on the nodes of a {@link JSONObject} while navigating its branches.
+ *
+ * See package-info for more details
+ *
+ * @author adoneitan@gmail.com
+ * @since 15 June 2016.
+ */
+public interface JSONNavigateAction extends NavigateAction
+{
+
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/navigate/JSONNavigator.java b/json-smart/src/main/java/net/minidev/json/actions/navigate/JSONNavigator.java
new file mode 100644
index 00000000..3a366c21
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/navigate/JSONNavigator.java
@@ -0,0 +1,24 @@
+package net.minidev.json.actions.navigate;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+
+import java.util.List;
+
+/**
+ * @author adoneitan@gmail.com
+ * @since 15 June 2016.
+ */
+public class JSONNavigator extends TreeNavigator
+{
+
+ public JSONNavigator(JSONNavigateAction action, List pathsToNavigate)
+ {
+ super(action, pathsToNavigate);
+ }
+
+ public JSONNavigator(JSONNavigateAction action, String... pathsToNavigate)
+ {
+ super(action, pathsToNavigate);
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/navigate/NavigateAction.java b/json-smart/src/main/java/net/minidev/json/actions/navigate/NavigateAction.java
new file mode 100755
index 00000000..51195e1a
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/navigate/NavigateAction.java
@@ -0,0 +1,111 @@
+package net.minidev.json.actions.navigate;
+
+import net.minidev.json.actions.path.TreePath;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An interface for a processing action on the nodes of a {@link M} while navigating its branches.
+ *
+ * See package-info for more details
+ *
+ * @author adoneitan@gmail.com
+ * @since 15 June 2016
+ */
+public interface NavigateAction, L extends List>
+{
+ /**
+ * called before navigation of a new path starts
+ * @return true if the specified path should be navigated
+ */
+ boolean pathStart(String path);
+
+ /**
+ * called before any navigation of the {@link M} starts
+ * @return true if navigation should start at all
+ */
+ boolean start(M objectToNavigate, Collection pathsToNavigate);
+
+ /**
+ * reached end of branch in source before end of specified path -
+ * the specified path does not exist in the source
+ */
+ void pathTailNotFound(TreePath tp, Object source);
+
+ /**
+ * called after the navigation of a path ends
+ */
+ void pathEnd(String path);
+
+ /**
+ * called if navigation of a path throws an exception
+ * @return true if the failure on this path should not abort the rest of the navigation
+ */
+ boolean failSilently(String path, Exception e);
+
+ /**
+ * called if navigation of a path throws an exception
+ * @return true if the failure on this path should abort the rest of the navigation
+ */
+ boolean failFast(String path, Exception e);
+
+ /**
+ * called when an object node is encountered on the path
+ * @return true if the navigator should navigate into the object
+ */
+ boolean recurInto(TreePath tp, M sourceNode);
+
+ /**
+ * called when an array node is encountered on the path
+ * @return true if the navigator should navigate into the array
+ */
+ boolean recurInto(TreePath tp, L sourceNode);
+
+ /**
+ * reached leaf node (not a container) in source but specified path expects children -
+ * the specified path does not exist in the source
+ */
+ void foundLeafBeforePathEnd(TreePath jp, Object obj);
+
+ /**
+ * called when a leaf node is reached in a M.
+ * a leaf in a M is a key-value pair where the value is not a container itself
+ * (it is not a M nor a L)
+ * @param tp - the JsonPath pointing to the leaf
+ */
+ void handleLeaf(TreePath tp, Object value);
+
+ /**
+ * called when a leaf in a L is reached.
+ * a leaf in a L is a non-container item
+ * (it is not a M nor a L)
+ * @param arrIndex - the index of the item in the L
+ * @param arrItem - the item
+ */
+ void handleLeaf(TreePath tp, int arrIndex, Object arrItem);
+
+ /**
+ * called when navigation of an {@link M} type object ends
+ * @param tp the path pointing to the object
+ */
+ void recurEnd(TreePath tp, M m);
+
+ /**
+ * called when navigation of an {@link L} type object ends
+ * @param tp the path pointing to the object
+ */
+ void recurEnd(TreePath tp, L l);
+
+ /**
+ * called after all navigation ends, and just before the navigation method exits
+ */
+ void end();
+
+ /**
+ * holds the result of the navigation, as assigned by the action implementing this interface
+ */
+ Object result();
+
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/navigate/TreeNavigator.java b/json-smart/src/main/java/net/minidev/json/actions/navigate/TreeNavigator.java
new file mode 100755
index 00000000..fba89fe8
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/navigate/TreeNavigator.java
@@ -0,0 +1,172 @@
+package net.minidev.json.actions.navigate;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.path.DotDelimiter;
+import net.minidev.json.actions.path.TreePath;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Navigates only the branches of a {@link JSONObject} corresponding to the paths specified.
+ *
+ * For each specified path to navigate, the {@link TreeNavigator} only traverses the matching
+ * branch.
+ *
+ * The navigator accepts an action and provides callback hooks for it to act on the traversed
+ * nodes at each significant step. See {@link NavigateAction}.
+ *
+ * See package-info for more details
+ *
+ * Example:
+ *
+ * To navigate the branch k1.k2 of the object {"k1":{"k2":"v1"}, "k3":{"k4":"v2"}} instantiate
+ * the navigator like so: new JSONNavigator("k1.k2")
+ *
+ * @author adoneitan@gmail.com
+ * @since 15 June 2016.
+ *
+ */
+public class TreeNavigator, L extends List>
+{
+ protected List pathsToNavigate;
+ protected NavigateAction action;
+ protected String pathPrefix = "";
+
+ public TreeNavigator(NavigateAction action, List pathsToNavigate)
+ {
+ if (action == null) {
+ throw new IllegalArgumentException("NavigateAction cannot be null");
+ }
+ this.action = action;
+ this.pathsToNavigate = pathsToNavigate;
+ }
+
+ public TreeNavigator with(String pathPrefix) {
+ this.pathPrefix = pathPrefix;
+ return this;
+ }
+
+ public TreeNavigator(NavigateAction action, String... pathsToNavigate)
+ {
+ this(action, Arrays.asList(pathsToNavigate));
+ }
+
+ public void nav(M object) throws Exception
+ {
+ if (action.start(object, pathsToNavigate))
+ {
+ for (String path: pathsToNavigate)
+ {
+ try
+ {
+ if (path != null && !path.equals("") && action.pathStart(path))
+ {
+ TreePath jp = new TreePath(path, new DotDelimiter().withAcceptDelimiterInNodeName(true));
+ nav(jp, object);
+ action.pathEnd(path);
+ }
+ }
+ catch (Exception e)
+ {
+ if (action.failSilently(path ,e)) {
+ break;
+ }
+ else if (action.failFast(path, e)) {
+ throw e;
+ }
+ }
+ }
+ }
+ action.end();
+ }
+
+ public void nav(TreePath jp, M map)
+ {
+ if (map == null || !action.recurInto(jp, map))
+ {
+ //source is null - navigation impossible
+ return;
+ }
+
+ if (jp.hasNext())
+ {
+ String key = jp.next();
+ if (!map.containsKey(key))
+ {
+ // cannot find next element of path in the source -
+ // the specified path does not exist in the source
+ action.pathTailNotFound(jp, map);
+ }
+ else if (map.get(key) instanceof Map)
+ {
+ //reached Map type node - handle it and recur into it
+ nav(jp, (M) map.get(key));
+ }
+ else if (map.get(key) instanceof List)
+ {
+ //reached List type node - handle it and recur into it
+ nav(jp, (L) map.get(key));
+ }
+ else if (jp.hasNext())
+ {
+ // reached leaf node (not a container) in source but specified path expects children -
+ // the specified path is illegal because it does not exist in the source.
+ action.foundLeafBeforePathEnd(jp, map.get(key));
+ }
+ else if (!jp.hasNext())
+ {
+ //reached leaf in source and specified path is also at leaf -> handle it
+ action.handleLeaf(jp, map.get(key));
+ }
+ else
+ {
+ throw new IllegalStateException("fatal: unreachable code reached at '" + jp.origin() + "'");
+ }
+ }
+ action.recurEnd(jp, (M) map);
+ }
+
+ public void nav(TreePath jp, L list)
+ {
+ if (list == null || !action.recurInto(jp, (L) list))
+ {
+ //list is null - navigation impossible
+ return;
+ }
+ int arrIndex = 0;
+ for (Object arrItem : list.toArray())
+ {
+ if (arrItem instanceof Map)
+ {
+ // clone the path so that for each object in the array,
+ // the iterator continues from the same position in the path
+ TreePath jpClone = getClone(jp);
+ nav(jpClone, (M) arrItem);
+ }
+ else if (arrItem instanceof List)
+ {
+ nav(jp, (L) arrItem);
+ }
+ else if (!jp.hasNext())
+ {
+ //reached leaf - handle it
+ action.handleLeaf(jp, arrIndex, arrItem);
+ }
+ arrIndex++;
+ }
+ action.recurEnd(jp, (L) list);
+ }
+
+ private TreePath getClone(TreePath jp)
+ {
+ try
+ {
+ return jp.clone();
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("failed to clone path", e);
+ }
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/navigate/package-info.java b/json-smart/src/main/java/net/minidev/json/actions/navigate/package-info.java
new file mode 100644
index 00000000..40727e40
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/navigate/package-info.java
@@ -0,0 +1,28 @@
+/**
+ * Navigate user-specified paths in a tree made up of {@link java.util.Map}s and {@link java.util.List} and process them
+ *
+ * {@link net.minidev.json.actions.navigate.TreeNavigator} only navigates through branches corresponding
+ * to user-specified paths. For each path, the navigation starts at the root and moves down the branch.
+ *
+ * The {@link net.minidev.json.actions.navigate.TreeNavigator} accepts a
+ * {@link net.minidev.json.actions.navigate.NavigateAction} and provides callback hooks at each significant
+ * step which the {@link net.minidev.json.actions.navigate.NavigateAction} may use to process
+ * the nodes.
+ *
+ * A path to navigate must be specified in the n-gram format - a list of keys from the root down separated by dots:
+ * K0[[[[.K1].K2].K3]...]
+ *
+ * A key to the right of a dot is a direct child of a key to the left of a dot. Keys with a dot in their name are
+ * not supported.
+ *
+ * Sample usage:
+ *
+ * NavigateAction navAction = new NavigateAction(){...};
+ * JSONNavigator jsonNav = new JSONNavigator(navAction, "foo.bar.path");
+ * jsonNav.nav(new JSONObject(...));
+ * Object result = navAction.result();
+ *
+ *
+ * @author adoneitan@gmail.com
+ */
+package net.minidev.json.actions.navigate;
\ No newline at end of file
diff --git a/json-smart/src/main/java/net/minidev/json/actions/path/DotDelimiter.java b/json-smart/src/main/java/net/minidev/json/actions/path/DotDelimiter.java
new file mode 100644
index 00000000..2ea2bc07
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/path/DotDelimiter.java
@@ -0,0 +1,23 @@
+package net.minidev.json.actions.path;
+
+/**
+ * Encapsulates the delimiter '.' of the path parts when the path is specified in n-gram format.
+ * For example: root.node1.node11.leaf
+ *
+ * @author adoneitan@gmail.com
+ * @since 31 May2016
+ */
+public class DotDelimiter extends PathDelimiter
+{
+ protected static final char DELIM_CHAR = '.';
+
+ public DotDelimiter()
+ {
+ super(DELIM_CHAR);
+ }
+
+ public String regex()
+ {
+ return "\\.";
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/path/PathDelimiter.java b/json-smart/src/main/java/net/minidev/json/actions/path/PathDelimiter.java
new file mode 100644
index 00000000..2a9fae7d
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/path/PathDelimiter.java
@@ -0,0 +1,43 @@
+package net.minidev.json.actions.path;
+
+/**
+ * Encapsulates the delimiter of the path parts when given in n-gram format.
+ *
+ * @author adoneitan@gmail.com
+ * @since 31 May 2016
+ */
+public abstract class PathDelimiter
+{
+ protected char delimChar;
+ protected String delimStr;
+ protected boolean acceptDelimInKey;
+
+ public PathDelimiter(char delim)
+ {
+ this.delimChar = delim;
+ this.delimStr = "" + delim;
+ }
+
+ public PathDelimiter withAcceptDelimiterInNodeName(boolean acceptDelimInKey) {
+ this.acceptDelimInKey = acceptDelimInKey;
+ return this;
+ }
+
+ public boolean accept(String key)
+ {
+ if (!acceptDelimInKey && key.contains(delimStr))
+ return false;
+ return true;
+ }
+
+ public String str() {
+ return delimStr;
+ }
+
+ public char chr() {
+ return delimChar;
+ }
+
+ public abstract String regex();
+
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/path/SlashDelimiter.java b/json-smart/src/main/java/net/minidev/json/actions/path/SlashDelimiter.java
new file mode 100644
index 00000000..1c005f2f
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/path/SlashDelimiter.java
@@ -0,0 +1,24 @@
+package net.minidev.json.actions.path;
+
+/**
+ * Encapsulates the delimiter '.' of the path parts when the path is specified in n-gram format.
+ * For example: root.node1.node11.leaf
+ *
+ * @author adoneitan@gmail.com
+ * @since 31 May 2016
+ */
+public class SlashDelimiter extends PathDelimiter
+{
+ protected static final char DELIM_CHAR = '/';
+
+ public SlashDelimiter()
+ {
+ super(DELIM_CHAR);
+ super.withAcceptDelimiterInNodeName(false);
+ }
+
+ public String regex()
+ {
+ return "\\/";
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/path/TreePath.java b/json-smart/src/main/java/net/minidev/json/actions/path/TreePath.java
new file mode 100755
index 00000000..a2d4f0bd
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/path/TreePath.java
@@ -0,0 +1,248 @@
+package net.minidev.json.actions.path;
+
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * {@link TreePath} represents an n-gram formatted path
+ * corresponding to a branch in a tree of {@link Map}s
+ * and {@link List}s
+ *
+ * See package-info for more details
+ *
+ * @author adoneitan@gmail.com
+ */
+public class TreePath
+{
+
+ protected enum Step {NONE, NEXT, PREV}
+ protected final String path;
+ protected List keys;
+ protected ListIterator keysItr;
+ protected String currKey;
+ protected Step lastStep;
+ protected StringBuilder origin;
+ protected StringBuilder remainder;
+ protected PathDelimiter delim;
+
+ public TreePath(String path, PathDelimiter delim)
+ {
+ this.delim = delim;
+ checkPath(path);
+ this.path = path;
+ this.keys = Arrays.asList(path.split(delim.regex()));
+ reset();
+ }
+
+ public void reset()
+ {
+ keysItr = keys.listIterator();
+ currKey = "";
+ lastStep = Step.NONE;
+ origin = new StringBuilder("");
+ remainder = new StringBuilder(path);
+ }
+
+ public boolean hasNext() {
+ return keysItr.hasNext();
+ }
+
+ public int nextIndex() {
+ return keysItr.nextIndex();
+ }
+
+ public String next()
+ {
+ currKey = keysItr.next();
+ /** when changing direction the {@link ListIterator} does not really
+ * move backward so an extra step is performed */
+ if (!lastStep.equals(Step.PREV)) {
+ originIncrement();
+ remainderDecrement();
+ }
+ lastStep = Step.NEXT;
+ return currKey;
+ }
+
+ public boolean hasPrev() {
+ return keysItr.hasPrevious();
+ }
+
+ public int prevIndex() {
+ return keysItr.previousIndex();
+ }
+
+ public String prev()
+ {
+ String temp = currKey;
+ currKey = keysItr.previous();
+ /** when changing direction the {@link ListIterator} does not really
+ * move backward so an extra step is performed */
+ if (!lastStep.equals(Step.NEXT)) {
+ remainderIncrement(temp);
+ originDecrement();
+ }
+ lastStep = Step.PREV;
+ return currKey;
+ }
+
+ private void remainderDecrement()
+ {
+ if (length() == 1)
+ remainder = new StringBuilder("");
+ else if (remainder.indexOf(delim.str()) < 0)
+ remainder = new StringBuilder("");
+ else
+ remainder.delete(0, remainder.indexOf(delim.str()) + 1);
+ }
+
+ private void originDecrement()
+ {
+ if (length() == 1)
+ origin = new StringBuilder("");
+ else if (origin.indexOf(delim.str()) < 0)
+ origin = new StringBuilder("");
+ else
+ origin.delete(origin.lastIndexOf(delim.str()), origin.length());
+ }
+
+ private void originIncrement()
+ {
+ if (origin.length() != 0) {
+ origin.append(delim.chr());
+ }
+ origin.append(currKey);
+ }
+
+ private void remainderIncrement(String prev)
+ {
+ if (remainder.length() == 0)
+ remainder = new StringBuilder(prev);
+ else
+ remainder = new StringBuilder(prev).append(delim.chr()).append(remainder);
+ }
+
+ /**
+ * @return An n-gram path from the first key to the current key (inclusive)
+ */
+ public String path() {
+ return path;
+ }
+
+ /**
+ * @return An n-gram path from the first key to the current key (inclusive)
+ */
+ public String origin() {
+ return origin.toString();
+ }
+
+ /**
+ * @return An n-gram path from the current key to the last key (inclusive)
+ */
+ public String remainder() {
+ return remainder.toString();
+ }
+
+ /**
+ * @return first element in the JSONPath
+ */
+ public String first() {
+ return keys.get(0);
+ }
+
+ /**
+ * @return last element in the JSONPath
+ */
+ public String last() {
+ return keys.get(keys.size() - 1);
+ }
+
+ /**
+ * @return current element pointed to by the path iterator
+ */
+ public String curr() {
+ return currKey;
+ }
+
+ public int length() {
+ return keys.size();
+ }
+
+ public String subPath(int firstIndex, int lastIndex)
+ {
+ if (lastIndex < firstIndex) {
+ throw new IllegalArgumentException("bad call to subPath");
+ }
+ StringBuilder sb = new StringBuilder(path.length());
+ for (int i = firstIndex; i <= lastIndex; i++)
+ {
+ sb.append(keys.get(i));
+ if (i < lastIndex) {
+ sb.append(delim.chr());
+ }
+ }
+ sb.trimToSize();
+ return sb.toString();
+ }
+
+ private void checkPath(String path)
+ {
+ if (path == null || path.equals(""))
+ throw new IllegalArgumentException("path cannot be null or empty");
+ if (path.startsWith(delim.str()) || path.endsWith(delim.str()) || path.contains(delim.str() + delim.str()))
+ throw new IllegalArgumentException(String.format("path cannot start or end with %s or contain '%s%s'", delim.str(), delim.str(), delim.str()));
+ }
+
+ @Override
+ public TreePath clone() throws CloneNotSupportedException
+ {
+ TreePath cloned = new TreePath(this.path, this.delim);
+ while (cloned.nextIndex() != this.nextIndex()) {
+ cloned.next();
+ }
+ if (cloned.prevIndex() != this.prevIndex()) {
+ cloned.prev();
+ }
+ cloned.lastStep = this.lastStep;
+ cloned.currKey = new String(this.currKey);
+ cloned.origin = new StringBuilder(this.origin);
+ cloned.remainder = new StringBuilder(this.remainder);
+ return cloned;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TreePath treePath = (TreePath) o;
+
+ return path().equals(treePath.path()) &&
+ hasNext() == treePath.hasNext() &&
+ hasPrev() == treePath.hasPrev() &&
+ curr().equals(treePath.curr()) &&
+ origin().equals(treePath.origin()) &&
+ remainder().equals(treePath.remainder()) &&
+ lastStep == treePath.lastStep &&
+ delim.equals(treePath.delim);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = path.hashCode();
+ result = 31 * result + keys.hashCode();
+ result = 31 * result + keysItr.hashCode();
+ result = 31 * result + currKey.hashCode();
+ result = 31 * result + lastStep.hashCode();
+ result = 31 * result + origin.hashCode();
+ result = 31 * result + remainder.hashCode();
+ result = 31 * result + delim.hashCode();
+ return result;
+ }
+}
+
+
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/JSONTraverseAction.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/JSONTraverseAction.java
new file mode 100755
index 00000000..19aa6f20
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/JSONTraverseAction.java
@@ -0,0 +1,15 @@
+package net.minidev.json.actions.traverse;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+
+/**
+ * An interface for a processing action on the nodes of a {@link JSONObject} while traversing it.
+ *
+ * See package-info for more details
+ *
+ * @author adoneitan@gmail.com
+ */
+public interface JSONTraverseAction extends TreeTraverseAction
+{
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/JSONTraverser.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/JSONTraverser.java
new file mode 100755
index 00000000..eb55c092
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/JSONTraverser.java
@@ -0,0 +1,33 @@
+package net.minidev.json.actions.traverse;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.path.DotDelimiter;
+import net.minidev.json.actions.path.PathDelimiter;
+
+/**
+ * Traverses every node of a {@link JSONObject}
+ *
+ * {@link JSONTraverser} accepts an action and provides callback hooks for it to act
+ * on the traversed nodes at each significant step. See {@link JSONTraverseAction}.
+ *
+ * A key to the right of a dot is a direct child of a key to the left of a dot.
+ * Keys with a dot in their name are not supported.
+ *
+ * See package-info for more details
+ *
+ * @author adoneitan@gmail.com
+ *
+ */
+public class JSONTraverser extends TreeTraverser
+{
+ public JSONTraverser(JSONTraverseAction action)
+ {
+ super(action, new DotDelimiter());
+ }
+
+ public JSONTraverser with(PathDelimiter delim) {
+ super.delim = delim;
+ return this;
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/KeysPrintAction.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/KeysPrintAction.java
new file mode 100644
index 00000000..55b4c47a
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/KeysPrintAction.java
@@ -0,0 +1,67 @@
+package net.minidev.json.actions.traverse;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+
+import java.util.Map.Entry;
+
+/**
+ * @author adoneitan@gmail.com
+ * @since 5/24/16.
+ */
+public class KeysPrintAction implements JSONTraverseAction
+{
+ @Override
+ public boolean start(JSONObject object)
+ {
+ return true;
+ }
+
+ @Override
+ public boolean traverseEntry(String fullPathToEntry, Entry entry)
+ {
+ System.out.println(entry.getKey());
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String pathToEntry, JSONObject entryValue) {
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String pathToEntry, JSONArray entryValue) {
+ return true;
+ }
+
+ @Override
+ public void handleLeaf(String pathToEntry, Entry entry)
+ {
+
+ }
+
+ @Override
+ public void handleLeaf(String fullPathToContainingList, int listIndex, Object listItem)
+ {
+
+ }
+
+ @Override
+ public boolean removeEntry(String fullPathToEntry, Entry entry)
+ {
+ return false;
+ }
+
+ @Override
+ public void end()
+ {
+
+ }
+
+ @Override
+ public Object result()
+ {
+ return null;
+ }
+
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/LocatePathsJSONAction.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/LocatePathsJSONAction.java
new file mode 100755
index 00000000..f798c03e
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/LocatePathsJSONAction.java
@@ -0,0 +1,102 @@
+package net.minidev.json.actions.traverse;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.path.PathDelimiter;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Searches for paths in a {@link JSONObject} and returns those found
+ *
+ * A path is not removed from the user-specified list once its processing is over,
+ * because identical objects in the same array are supported by this action.
+ *
+ * See package-info for more details
+ *
+ * See unit tests for examples
+ *
+ * @author adoneitan@gmail.com
+ *
+ */
+public class LocatePathsJsonAction implements JSONTraverseAction
+{
+ protected List pathsFound;
+ protected List pathsToFind;
+ protected PathDelimiter delim;
+
+ /**
+ *
+ * @param pathsToFind A path to a field in the {@link JSONObject} should be specified in n-gram format where keys are chained:
+ * k0[[[.k1].k2]...]
+ */
+ public LocatePathsJsonAction(List pathsToFind, PathDelimiter delim)
+ {
+ this.pathsToFind = pathsToFind;
+ this.delim = delim;
+ pathsFound = new LinkedList();
+ }
+
+ @Override
+ public boolean start(JSONObject object)
+ {
+ return object != null && pathsToFind != null && pathsToFind.size() > 0;
+ }
+
+ @Override
+ public boolean traverseEntry(String fullPathToEntry, Entry entry)
+ {
+ if (!delim.accept(entry.getKey())) {
+ return false;
+ }
+ locatePath(fullPathToEntry);
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String pathToEntry, JSONObject entryValue) {
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String pathToEntry, JSONArray entryValue) {
+ return true;
+ }
+
+ @Override
+ public void handleLeaf(String pathToEntry, Entry entry) {
+
+ }
+
+ @Override
+ public void handleLeaf(String fullPathToContainingList, int listIndex, Object listItem) {
+
+ }
+
+ @Override
+ public boolean removeEntry(String fullPathToEntry, Entry entry)
+ {
+ return false;
+ }
+
+ @Override
+ public void end() {
+ //nothing to do
+ }
+
+ @Override
+ public Object result() {
+ return pathsFound;
+ }
+
+ private void locatePath(String pathToEntry)
+ {
+ if (pathsToFind.contains(pathToEntry))
+ {
+ //reached end of path that is being searched
+ pathsFound.add(pathToEntry);
+ }
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/RemoveElementsJSONAction.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/RemoveElementsJSONAction.java
new file mode 100755
index 00000000..f8a4d7d4
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/RemoveElementsJSONAction.java
@@ -0,0 +1,90 @@
+package net.minidev.json.actions.traverse;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Removes key:value elements from a {@link JSONObject}.
+ *
+ * An element is not removed from the user-specified list once its processing is over,
+ * because it may appear in more than one node.
+ *
+ * See package-info for more details
+ *
+ * See unit tests for examples
+ *
+ * @author adoneitan@gmail.com
+ *
+ */
+public class RemoveElementsJsonAction implements JSONTraverseAction
+{
+ protected JSONObject result;
+ protected final Map elementsToRemove;
+ protected final boolean allowDotChar;
+
+ public RemoveElementsJsonAction(Map elementsToRemove, boolean allowDotChar)
+ {
+ this.elementsToRemove = elementsToRemove;
+ this.allowDotChar = allowDotChar;
+ }
+
+ public RemoveElementsJsonAction(Map elementsToRemove)
+ {
+ this(elementsToRemove, false);
+ }
+
+ @Override
+ public boolean start(JSONObject object)
+ {
+ result = object;
+ return object != null && elementsToRemove != null && elementsToRemove.size() > 0;
+ }
+
+ @Override
+ public boolean removeEntry(String fullPathToEntry, Entry entry)
+ {
+ return elementsToRemove.entrySet().contains(entry);
+ }
+
+ @Override
+ public boolean traverseEntry(String fullPathToEntry, Entry entry)
+ {
+ //must traverse the whole object
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String pathToEntry, JSONObject entryValue) {
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String pathToEntry, JSONArray entryValue) {
+ return true;
+ }
+
+ @Override
+ public void handleLeaf(String pathToEntry, Entry entry)
+ {
+
+ }
+
+ @Override
+ public void handleLeaf(String fullPathToContainingList, int listIndex, Object listItem)
+ {
+
+ }
+
+ @Override
+ public void end() {
+ //nothing to do
+ }
+
+ @Override
+ public Object result() {
+ return result;
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/RemovePathsJsonAction.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/RemovePathsJsonAction.java
new file mode 100755
index 00000000..fac23fd5
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/RemovePathsJsonAction.java
@@ -0,0 +1,83 @@
+package net.minidev.json.actions.traverse;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Removes branches from a {@link JSONObject}.
+ *
+ * A path is not removed from the user-specified list once its processing is over,
+ * because identical objects in the same array are supported by this action.
+ *
+ * See package-info for more details
+ *
+ * See unit tests for examples
+ *
+ * @author adoneitan@gmail.com
+ *
+ */
+public class RemovePathsJsonAction implements JSONTraverseAction
+{
+ protected JSONObject result;
+ protected List pathsToRemove;
+
+ public RemovePathsJsonAction(List pathsToRemove)
+ {
+ this.pathsToRemove = pathsToRemove;
+ }
+
+ @Override
+ public boolean start(JSONObject object)
+ {
+ result = object;
+ return object != null && pathsToRemove != null && pathsToRemove.size() > 0;
+ }
+
+ @Override
+ public boolean removeEntry(String fullPathToEntry, Entry entry)
+ {
+ return pathsToRemove.contains(fullPathToEntry);
+ }
+
+ @Override
+ public boolean traverseEntry(String fullPathToEntry, Entry entry)
+ {
+ //must traverse the whole object
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String pathToEntry, JSONObject entryValue) {
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String pathToEntry, JSONArray entryValue) {
+ return true;
+ }
+
+ @Override
+ public void handleLeaf(String pathToEntry, Entry entry)
+ {
+
+ }
+
+ @Override
+ public void handleLeaf(String fullPathToContainingList, int listIndex, Object listItem)
+ {
+
+ }
+
+ @Override
+ public void end() {
+ //nothing to do
+ }
+
+ @Override
+ public Object result() {
+ return result;
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/RetainPathsJsonAction.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/RetainPathsJsonAction.java
new file mode 100755
index 00000000..1948da93
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/RetainPathsJsonAction.java
@@ -0,0 +1,119 @@
+package net.minidev.json.actions.traverse;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.actions.path.PathDelimiter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Retain branches or parts of branches matching a specified list of paths.
+ *
+ * Paths are matched from the root down. If a user-specified path ends at a non-leaf node,
+ * the rest of the branch from that node to the leaf is not retained.
+ *
+ * A path is not removed from the user-specified list once its processing is over,
+ * because identical objects in the same array are supported by this action.
+ *
+ * See package-info for more details
+ *
+ * See unit tests for examples
+ *
+ * @author adoneitan@gmail.com
+ *
+ */
+public class RetainPathsJsonAction implements JSONTraverseAction
+{
+ protected final PathDelimiter delim;
+ protected JSONObject result;
+ protected List pathsToRetain;
+
+ public RetainPathsJsonAction(List pathsToRetain, PathDelimiter delim)
+ {
+ this.pathsToRetain = new ArrayList(pathsToRetain);
+ this.delim = delim;
+ }
+
+ @Override
+ public boolean start(JSONObject object)
+ {
+ if (object == null)
+ {
+ result = null;
+ return false;
+ }
+ if (pathsToRetain == null || pathsToRetain.size() == 0)
+ {
+ result = new JSONObject();
+ return false;
+ }
+ result = object;
+ return true;
+ }
+
+ @Override
+ public boolean traverseEntry(String fullPathToEntry, Entry entry) {
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String fullPathToSubtree, JSONObject entryValue) {
+ return true;
+ }
+
+ @Override
+ public boolean recurInto(String fullPathToArrayItem, JSONArray entryValue) {
+ return true;
+ }
+
+ @Override
+ public void handleLeaf(String pathToEntry, Entry entry) {
+ }
+
+ @Override
+ public void handleLeaf(String fullPathToContainingList, int listIndex, Object listItem) {
+ }
+
+ @Override
+ public boolean removeEntry(String fullPathToEntry, Entry entry) {
+ return discardPath(fullPathToEntry, entry);
+ }
+
+ @Override
+ public void end()
+ {
+ // nothing to do
+ }
+
+ @Override
+ public Object result() {
+ return result;
+ }
+
+ /**
+ * if the full path to the entry is not contained in any of the paths to retain - remove it from the object
+ * this step does not remove entries whose full path is contained in a path to retain but are not equal to an
+ * entry to retain
+ */
+ protected boolean discardPath(String pathToEntry, Entry entry)
+ {
+ if (!foundAsPrefix(pathToEntry) || !delim.accept(entry.getKey()))
+ {
+ //skip traversal of subtree and remove from the traversal iterator
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean foundAsPrefix(String path)
+ {
+ for (String p : pathsToRetain) {
+ if (p == path || (p != null && path != null && p.startsWith(path))) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/TreeTraverseAction.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/TreeTraverseAction.java
new file mode 100755
index 00000000..61922c8d
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/TreeTraverseAction.java
@@ -0,0 +1,73 @@
+package net.minidev.json.actions.traverse;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * An interface for a processing action on the nodes of a {@link M} tree
+ * while traversing it. The order in which the callbacks are listed
+ * below is the order in which they are called by the {@link TreeTraverser}
+ *
+ * See package-info for more details
+ *
+ * @author adoneitan@gmail.com
+ */
+public interface TreeTraverseAction, L extends List>
+{
+ /**
+ * called before any traversal of the {@link M} tree starts
+ * @return true if traversal should start at all
+ */
+ boolean start(M object);
+
+ /**
+ * called when a new entry is encountered and before any processing is performed on it
+ * @return true if the entry should be processed
+ */
+ boolean traverseEntry(String fullPathToEntry, Entry entry);
+
+ /**
+ * the last callback for each entry in an {@link M} map. if this method returns true
+ * the {@link TreeTraverser} removes the entry from the map. there is no further
+ * handling of the entry.
+ * @return true if the entry and its subtree should be removed from the {@link M} tree
+ */
+ boolean removeEntry(String fullPathToEntry, Entry entry);
+
+ /**
+ * called when a non-leaf entry is encountered inside an {@Link M} object
+ * @return true if the non-leaf entry should be recursively traversed
+ */
+ boolean recurInto(String fullPathToSubtree, M entryValue);
+
+ /**
+ * called when a non-leaf item is encountered inside an {@Link L} object
+ * @return true if the non-leaf item should be recursively traversed
+ */
+ boolean recurInto(String fullPathToContainingList, L entryValue);
+
+ /**
+ * called for each leaf of an {@link M} map is encountered
+ */
+ void handleLeaf(String fullPathToEntry, Entry entry);
+
+ /**
+ * called for each leaf of an {@link L} list is encountered
+ * @param listItem - the item
+ * @param listIndex - the ordered location of the item in the list
+ */
+ void handleLeaf(String fullPathToContainingList, int listIndex, Object listItem);
+
+ /**
+ * called after the traversal ends,
+ * and just before the {@link #start(M)} method exits
+ */
+ void end();
+
+ /**
+ * holds the result of the traversal,
+ * as assigned by the action implementing this interface
+ */
+ Object result();
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/TreeTraverser.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/TreeTraverser.java
new file mode 100755
index 00000000..39fbbfcf
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/TreeTraverser.java
@@ -0,0 +1,109 @@
+package net.minidev.json.actions.traverse;
+
+import net.minidev.json.actions.path.PathDelimiter;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Traverses every node of a tree made up of a combination of {@link Map}s and {@link List}s
+ *
+ * {@link TreeTraverser} accepts an action and provides callback hooks for it to act
+ * on the traversed nodes at each significant step. See {@link TreeTraverseAction}.
+ *
+ * See package-info for more details
+ *
+ * @author adoneitan@gmail.com
+ *
+ */
+public class TreeTraverser, L extends List>
+{
+ protected TreeTraverseAction action;
+ protected PathDelimiter delim;
+ protected String pathPrefix = "";
+
+ public TreeTraverser(TreeTraverseAction action, PathDelimiter delim)
+ {
+ this.action = action;
+ this.delim = delim;
+ }
+
+ public TreeTraverser with(String pathPrefix) {
+ this.pathPrefix = pathPrefix;
+ return this;
+ }
+
+ public void traverse(M map)
+ {
+ if (action.start(map)){
+ depthFirst(pathPrefix, map);
+ }
+ action.end();
+ }
+
+ private void depthFirst(String fullPath, M map)
+ {
+ if (map == null || map.entrySet() == null || !action.recurInto(fullPath, map)) {
+ return;
+ }
+ Iterator> it = map.entrySet().iterator();
+ while (it.hasNext())
+ {
+ Entry entry = it.next();
+ String fullPathToEntry = buildPath(fullPath, entry.getKey());
+
+ if (!action.traverseEntry(fullPathToEntry, entry)) {
+ continue;
+ }
+ else if (action.removeEntry(fullPathToEntry, entry))
+ {
+ it.remove();
+ continue;
+ }
+
+ if (entry.getValue() instanceof Map)
+ {
+ depthFirst(fullPathToEntry, (M) entry.getValue());
+ }
+ else if (entry.getValue() instanceof List)
+ {
+ depthFirst(fullPathToEntry, (L) entry.getValue());
+ }
+ else
+ {
+ action.handleLeaf(fullPathToEntry, entry);
+ }
+ }
+ }
+
+ private void depthFirst(String fullPath, L list)
+ {
+ if (!action.recurInto(fullPath, (L) list)) {
+ return;
+ }
+ int listIndex = 0;
+ for (Object listItem : list.toArray())
+ {
+ if (listItem instanceof Map)
+ {
+ depthFirst(fullPath, (M) listItem);
+ }
+ else if (listItem instanceof List)
+ {
+ depthFirst(fullPath, (L) listItem);
+ }
+ else
+ {
+ action.handleLeaf(fullPath, listIndex, listItem);
+ }
+ listIndex++;
+ }
+ }
+
+ private String buildPath(String fullPath, String entryKey) {
+ return pathPrefix.equals(fullPath) ? pathPrefix + entryKey : fullPath + delim.str() + entryKey;
+ }
+
+}
diff --git a/json-smart/src/main/java/net/minidev/json/actions/traverse/package-info.java b/json-smart/src/main/java/net/minidev/json/actions/traverse/package-info.java
new file mode 100644
index 00000000..5fba45b8
--- /dev/null
+++ b/json-smart/src/main/java/net/minidev/json/actions/traverse/package-info.java
@@ -0,0 +1,31 @@
+/**
+ *
+ * Traverse all the nodes in a {@link net.minidev.json.JSONObject} and process them
+ *
+ * The traversal starts at the root and moves breadth-first down the branches.
+ * The {@link net.minidev.json.actions.traverse.TreeTraverser} accepts a
+ * {@link net.minidev.json.actions.traverse.JSONTraverseAction} and provides callback hooks at each significant
+ * step which the {@link net.minidev.json.actions.traverse.JSONTraverseAction} may use to process
+ * the nodes.
+ *
+ * The {@link net.minidev.json.actions.traverse.TreeTraverser} assumes that paths in the tree which the
+ * {@link net.minidev.json.JSONObject} represents are specified in the n-gram format - a list of keys from the
+ * root down separated by dots:
+ *
+ * K0[[[[.K1].K2].K3]...]
+ *
+ * A key to the right of a dot is a direct child of a key to the left of a dot.
+ * Keys with a dot in their name are not supported.
+ *
+ * Sample usage:
+ *
+ * TraverseAction tAction = new TraverseAction(){...};
+ * JSONTraverser jsonTrv = new JSONTraverser(tAction);
+ * jsonTrv.traverse(new JSONObject(...));
+ * Object result = tAction.result();
+ *
+ *
+
+ * @author adoneitan@gmail.com
+ */
+package net.minidev.json.actions.traverse;
\ No newline at end of file
diff --git a/json-smart/src/test/java/net/minidev/json/test/actions/ElementRemoverTest.java b/json-smart/src/test/java/net/minidev/json/test/actions/ElementRemoverTest.java
new file mode 100755
index 00000000..327fa2d1
--- /dev/null
+++ b/json-smart/src/test/java/net/minidev/json/test/actions/ElementRemoverTest.java
@@ -0,0 +1,106 @@
+package net.minidev.json.test.actions;
+
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.minidev.json.actions.ElementRemover;
+import net.minidev.json.parser.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests {@link ElementRemover}
+ *
+ * @author adoneitan@gmail.com
+ */
+@RunWith(Parameterized.class)
+public class ElementRemoverTest
+{
+ private String jsonToClean;
+ private String elementsToRemove;
+ private String expectedJson;
+
+ public ElementRemoverTest(String jsonToClean, String elementsToRemove, String expectedJson)
+ {
+ this.jsonToClean = jsonToClean;
+ this.elementsToRemove = elementsToRemove;
+ this.expectedJson = expectedJson;
+ }
+
+ @Parameterized.Parameters
+ public static Collection params()
+ {
+ return Arrays.asList(new Object[][]{
+
+ {"{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}", null, "{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}"},
+ {"{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}", "{}", "{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}"},
+ {"{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}", "{\"k0\":\"v2\"}", "{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}"},
+ {"{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}", "{\"k2\":\"v2\"}", "{\"k0\":{},\"k1\":{\"k3\":\"v3\"}}"},
+ {"{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}", "{\"k0\":{\"k2\":\"v2\"}}", "{\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}"},
+ {"{\"k0\":{\"k2\":\"v2\"},\"k1\":{\"k2\":\"v2\",\"k3\":\"v3\"}}", "{\"k2\":\"v2\",\"k3\":\"v3\"}", "{\"k0\":{},\"k1\":{}}"},
+ {"{\"k0\":{}}", "{}", "{\"k0\":{}}"},
+ });
+ }
+
+ @Test
+ public void test() throws ParseException
+ {
+ JSONObject objectToClean = jsonToClean != null ? (JSONObject) JSONValue.parseWithException(jsonToClean) : null;
+ JSONObject expectedObject = expectedJson != null ? (JSONObject) JSONValue.parseWithException(expectedJson): null;
+ JSONObject toRemove = elementsToRemove != null ? (JSONObject) JSONValue.parseWithException(elementsToRemove): null;
+ ElementRemover er = new ElementRemover(toRemove);
+ er.remove(objectToClean);
+ assertEquals(expectedObject, objectToClean);
+ }
+
+// private ElementRemover switchKeyToRemove()
+// {
+// long m = System.currentTimeMillis();
+// if (elementsToRemove == null && m % 4 == 0)
+// {
+// System.out.println("cast to String");
+// return new ElementRemover((String)null);
+// }
+// else if (elementsToRemove == null && m % 4 == 1)
+// {
+// System.out.println("cast to String[]");
+// return new ElementRemover((String[])null);
+// }
+// else if (elementsToRemove == null && m % 4 == 2)
+// {
+// System.out.println("cast to JSONArray");
+// return new ElementRemover((JSONArray)null);
+// }
+// else if (elementsToRemove == null && m % 4 == 3)
+// {
+// System.out.println("cast to List");
+// return new ElementRemover((List)null);
+// }
+// else if (elementsToRemove instanceof String)
+// {
+// return new ElementRemover((String) elementsToRemove);
+// }
+// else if (elementsToRemove instanceof String[])
+// {
+// return new ElementRemover((String[]) elementsToRemove);
+// }
+// else if (elementsToRemove instanceof JSONArray)
+// {
+// return new ElementRemover((JSONArray) elementsToRemove);
+// }
+// else if (elementsToRemove instanceof List>)
+// {
+// return new ElementRemover((List) elementsToRemove);
+// }
+// else
+// {
+// throw new IllegalArgumentException("bad test setup: wrong type of key to remove");
+// }
+// }
+
+}
diff --git a/json-smart/src/test/java/net/minidev/json/test/actions/KeysPrintActionTest.java b/json-smart/src/test/java/net/minidev/json/test/actions/KeysPrintActionTest.java
new file mode 100644
index 00000000..52dce46d
--- /dev/null
+++ b/json-smart/src/test/java/net/minidev/json/test/actions/KeysPrintActionTest.java
@@ -0,0 +1,45 @@
+package net.minidev.json.test.actions;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.minidev.json.actions.traverse.JSONTraverser;
+import net.minidev.json.actions.traverse.KeysPrintAction;
+import net.minidev.json.parser.ParseException;
+import org.junit.Test;
+
+/**
+ * @author adoneitan@gmail.com
+ * @since 30 May 2016
+ */
+public class KeysPrintActionTest
+{
+ @Test
+ public void test() throws ParseException
+ {
+ KeysPrintAction p = new KeysPrintAction();
+ JSONTraverser t = new JSONTraverser(p);
+ JSONObject jo = (JSONObject) JSONValue.parseWithException(
+ "{" +
+ "\"k0\":{" +
+ "\"k01\":{" +
+ "\"k011\":\"v2\"" +
+ "}" +
+ "}," +
+ "\"k1\":{" +
+ "\"k11\":{" +
+ "\"k111\":\"v5\"" +
+ "}," +
+ "\"k12\":{" +
+ "\"k121\":\"v5\"" +
+ "}" +
+ "}," +
+ "\"k3\":{" +
+ "\"k31\":{" +
+ "\"k311\":\"v5\"" +
+ "}" +
+ "}" +
+ "}"
+ );
+ t.traverse(jo);
+ }
+}
\ No newline at end of file
diff --git a/json-smart/src/test/java/net/minidev/json/test/actions/PathLocatorTest.java b/json-smart/src/test/java/net/minidev/json/test/actions/PathLocatorTest.java
new file mode 100755
index 00000000..29734110
--- /dev/null
+++ b/json-smart/src/test/java/net/minidev/json/test/actions/PathLocatorTest.java
@@ -0,0 +1,159 @@
+package net.minidev.json.test.actions;
+
+import net.minidev.json.actions.PathLocator;
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.minidev.json.parser.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author adoneitan@gmail.com
+ */
+@RunWith(Parameterized.class)
+public class PathLocatorTest
+{
+ private String jsonToSearch;
+ private Object keysToFind;
+ private String[] expectedFound;
+
+ public PathLocatorTest(String jsonToSearch, Object keysToFind, String[] expectedFound)
+ {
+ this.jsonToSearch = jsonToSearch;
+ this.keysToFind = keysToFind;
+ this.expectedFound = expectedFound;
+ }
+
+ @Parameterized.Parameters
+ public static Collection params()
+ {
+ return Arrays.asList(new Object[][]{
+
+ //nulls, bad/empty keys
+ {null, null, new String[]{} },
+ {null, "", new String[]{} },
+ {null, "k1", new String[]{} },
+ {null, new String[]{}, new String[]{} },
+ {null, new JSONArray(), new String[]{} },
+ {null, new ArrayList(0), new String[]{} },//5
+
+ //empty json, bad/empty keys
+ {"{}", null, new String[]{} },
+ {"{}", "", new String[]{} },
+ {"{}", "k1", new String[]{} },
+ {"{}", new String[]{}, new String[]{} },
+ {"{}", new JSONArray(), new String[]{} },//10
+ {"{}", new ArrayList(0), new String[]{} },
+
+ //simple json, bad/empty keys
+ {"{\"k0\":\"v0\"}", null, new String[]{} },
+ {"{\"k0\":\"v0\"}", "", new String[]{} },
+ {"{\"k0\":\"v0\"}", "k1", new String[]{} },
+ {"{\"k0\":\"v0\"}", new String[]{}, new String[]{} },//15
+ {"{\"k0\":\"v0\"}", new JSONArray(), new String[]{} },
+ {"{\"k0\":\"v0\"}", new ArrayList(0), new String[]{} },
+
+ //simple json, valid/invalid keys
+ {"{\"k0\":\"v0\"}", "k0", new String[]{"k0"} },
+ {"{\"k0\":\"v0\"}", "v0", new String[]{} },
+ {"{\"k0\":\"v0\"}", "k0.k1", new String[]{} },//20
+ {"{\"k0\":\"v0\"}", "k1.k0", new String[]{} },
+ {"{\"k0\":null}", "k0", new String[]{"k0"} },
+ {"{\"k0\":null}", null, new String[]{} },
+
+ //key with dot char
+ {"{\"k0.k1\":\"v0\"}", "k0", new String[]{} },
+ {"{\"k0.k1\":\"v0\"}", "k1", new String[]{} },//25
+ {"{\"k0.k1\":\"v0\"}", "k0.k1", new String[]{} },
+
+ // key with dot ambiguity
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k0", new String[]{"k0"} },
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k1", new String[]{} },
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k0.k1", new String[]{"k0.k1"} },
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k0.k1", new String[]{"k0.k1"} },//30
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k0.k1.k2", new String[]{"k0.k1.k2"} },
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k1.k2", new String[]{} },
+ {"{\"k0\":{\"k1.k2\":\"dot\"},\"k1\":{\"k2\":\"v2\"}}}","k0", new String[]{"k0"} },
+ {"{\"k0\":{\"k1.k2\":\"dot\"},\"k1\":{\"k2\":\"v2\"}}}","k1.k2", new String[]{"k1.k2"} },
+
+ //ignore non-existent keys but keep good keys
+ {"{\"k0\":\"v0\",\"k1\":\"v1\"}", new String[]{"k0","k2"}, new String[]{"k0"} },//35
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k2"}, new String[]{"k0"} },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1.k2"}, new String[]{"k0", "k1.k2"} },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1.k2.k3"}, new String[]{"k0"} },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1.k2","k1"}, new String[]{"k0","k1","k1.k2"} },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1","k0.k2"}, new String[]{"k0","k1"} },//40
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1","k2"}, new String[]{"k0","k1"} },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k1.k2"}, new String[]{"k1.k2"} },
+
+ //arrays - key inside array treated as child
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0", new String[]{"k0"} },
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1", new String[]{"k0.k1"} },
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1.k2", new String[]{"k0.k1.k2"} },//45
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"},{\"k2\":\"v2\"}]}}", "k0.k1.k2", new String[]{"k0.k1.k2", "k0.k1.k2"} },
+ });
+ }
+
+ @Test
+ public void test() throws ParseException
+ {
+ JSONObject objectToSearch = jsonToSearch != null ? (JSONObject) JSONValue.parseWithException(jsonToSearch) : null;
+ PathLocator locator = switchKeyToRemove();
+ List found = locator.locate(objectToSearch);
+ assertEquals(Arrays.asList(expectedFound), found);
+ }
+
+ private PathLocator switchKeyToRemove()
+ {
+ long m = System.currentTimeMillis();
+ if (keysToFind == null && m % 4 == 0)
+ {
+ System.out.println("cast to String");
+ return new PathLocator((String)null);
+ }
+ else if (keysToFind == null && m % 4 == 1)
+ {
+ System.out.println("cast to String[]");
+ return new PathLocator((String[])null);
+ }
+ else if (keysToFind == null && m % 4 == 2)
+ {
+ System.out.println("cast to JSONArray");
+ return new PathLocator((JSONArray)null);
+ }
+ else if (keysToFind == null && m % 4 == 3)
+ {
+ System.out.println("cast to List");
+ return new PathLocator((List)null);
+ }
+ else if (keysToFind instanceof String)
+ {
+ return new PathLocator((String) keysToFind);
+ }
+ else if (keysToFind instanceof String[])
+ {
+ return new PathLocator((String[]) keysToFind);
+ }
+ else if (keysToFind instanceof JSONArray)
+ {
+ return new PathLocator((JSONArray) keysToFind);
+ }
+ else if (keysToFind instanceof List>)
+ {
+ return new PathLocator((List) keysToFind);
+ }
+ else
+ {
+ throw new IllegalArgumentException("bad test setup: wrong type of key to remove");
+ }
+ }
+}
\ No newline at end of file
diff --git a/json-smart/src/test/java/net/minidev/json/test/actions/PathRemoverTest.java b/json-smart/src/test/java/net/minidev/json/test/actions/PathRemoverTest.java
new file mode 100755
index 00000000..2f18cfc7
--- /dev/null
+++ b/json-smart/src/test/java/net/minidev/json/test/actions/PathRemoverTest.java
@@ -0,0 +1,136 @@
+package net.minidev.json.test.actions;
+
+import net.minidev.json.actions.PathRemover;
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.minidev.json.parser.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests {@link PathRemover}
+ *
+ * @author adoneitan@gmail.com
+ */
+@RunWith(Parameterized.class)
+public class PathRemoverTest
+{
+ private String jsonToClean;
+ private Object keyToRemove;
+ private String expectedJson;
+
+ public PathRemoverTest(String jsonToClean, Object keyToRemove, String expectedJson)
+ {
+ this.jsonToClean = jsonToClean;
+ this.keyToRemove = keyToRemove;
+ this.expectedJson = expectedJson;
+ }
+
+ @Parameterized.Parameters
+ public static Collection params()
+ {
+ return Arrays.asList(new Object[][]{
+
+ {null, "key", null }, // null json
+ {"{}", "key", "{}" }, // empty json
+ {"{\"first\": null}", null, "{\"first\": null}" }, // null key
+ {"{\"first\": null}", "", "{\"first\": null}" }, // empty string key
+ {"{\"first\": null}", new String[]{}, "{\"first\": null}" }, // empty string array key
+ {"{\"first\": null}", new JSONArray(), "{\"first\": null}" }, // empty json array key
+ {"{\"first\": null}", new ArrayList(0), "{\"first\": null}" }, // empty list key
+ {"{\"first\": null}", "first", "{}" }, // remove root key
+ {"{\"first.f1\": null}", "first.f1", "{}" }, // key with dot
+ {"{\"first.f1\": \"withDot\", \"first\":{\"f1\": null}}", "first.f1", "{\"first\":{}}" }, //9 key with dot ambiguity
+ {"{\"first\":{\"f2\":{\"f3\":{\"id\":\"id1\"}}}}", "first.f2.f3.id", "{\"first\":{\"f2\":{\"f3\":{}}}}" }, // nested object remove single leaf
+ {"{\"first\":{\"f2\":{\"f3\":{\"id\":\"id1\"}}}}", "notfound", "{\"first\":{\"f2\":{\"f3\":{\"id\":\"id1\"}}}}" }, // nested object key does not exist
+ {"{\"first\":{\"f2\":{\"f3\":{\"id\":\"id1\",\"name\":\"me\"}}}}", "first.f2.f3.id", "{\"first\":{\"f2\":{\"f3\":{\"name\":\"me\"}}}}"}, // nested object remove first leaf
+ {"{\"first\":{\"f2\":{\"f3\":{\"id\":\"id1\",\"name\":\"me\"}}}}", "first.f2.f3.name", "{\"first\":{\"f2\":{\"f3\":{\"id\":\"id1\"}}}}" }, //13 nested object remove last leaf
+ {"{\"first\":{\"f2\":{\"f3\":{\"id\":\"id1\",\"name\":\"me\"}}}}", "first.f2.f3", "{\"first\":{\"f2\":{}}}" }, // nested object remove intermediate node
+ {"{\"first\":{\"f2\":{\"f3\":{\"id\":\"id1\",\"name\":\"me\"}}}}", "first", "{}" }, // nested object remove root
+ {"{\"first\":{\"f2\":[[1,{\"id\":\"id1\"},3],4]}}", "first.f2.id", "{\"first\":{\"f2\":[[1,{},3],4]}}" }, // double nested array remove leaf
+ {"{\"first\":{\"f2\":[[1,{\"id\":\"id1\"},3],4]}}", "first.f2", "{\"first\":{}}" }, // double nested array remove array
+ {"{\"first\":[[1,{\"id\":\"id1\"},3],4]}", "first", "{}" }, // double nested array remove root
+
+ //arrays
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1", "{\"k0\":{}}" }, // value is array
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1.k2", "{\"k0\":{\"k1\":[1,{},3,4]}}" }, // full path into array object
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1.3" , "{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}" }, // full path into array primitive
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},{\"k2\":\"v2\"},3,4]}}", "k0.k1.k2", "{\"k0\":{\"k1\":[1,{},{},3,4]}}" }, // full path into array with identical items
+
+ // composite json remove all roots
+ {"{\"first\": {\"f2\":{\"id\":\"id1\"}}, \"second\": [{\"k1\":{\"id\":\"id1\"}}, 4, 5, 6, {\"id\": 123}], \"third\": 789, \"id\": null}",
+ (JSONArray) JSONValue.parse("[\"first\",\"second\",\"third\",\"id\"]"),
+ "{}" },
+ // composite json remove all leaves
+ {"{\"first\": {\"f2\":{\"id\":\"id1\"}}, \"second\": [{\"k1\":{\"id\":\"id1\"}}, 4, 5, 6, {\"id\": 123}], \"third\": 789, \"id\": null}",
+ (List) Arrays.asList("first.f2.id", "second.k1.id", "second.id", "third", "id"),
+ "{\"first\": {\"f2\":{}}, \"second\": [{\"k1\":{}}, 4, 5, 6, {}]}" },
+
+ });
+ }
+
+ @Test
+ public void test() throws ParseException
+ {
+ JSONObject objectToClean = jsonToClean != null ? (JSONObject) JSONValue.parseWithException(jsonToClean) : null;
+ JSONObject expectedObject = expectedJson != null ? (JSONObject) JSONValue.parseWithException(expectedJson): null;
+ PathRemover cl = switchKeyToRemove();
+ cl.remove(objectToClean);
+ assertEquals(expectedObject, objectToClean);
+ }
+
+ private PathRemover switchKeyToRemove()
+ {
+ long m = System.currentTimeMillis();
+ if (keyToRemove == null && m % 4 == 0)
+ {
+ System.out.println("cast to String");
+ return new PathRemover((String)null);
+ }
+ else if (keyToRemove == null && m % 4 == 1)
+ {
+ System.out.println("cast to String[]");
+ return new PathRemover((String[])null);
+ }
+ else if (keyToRemove == null && m % 4 == 2)
+ {
+ System.out.println("cast to JSONArray");
+ return new PathRemover((JSONArray)null);
+ }
+ else if (keyToRemove == null && m % 4 == 3)
+ {
+ System.out.println("cast to List");
+ return new PathRemover((List)null);
+ }
+ else if (keyToRemove instanceof String)
+ {
+ return new PathRemover((String)keyToRemove);
+ }
+ else if (keyToRemove instanceof String[])
+ {
+ return new PathRemover((String[])keyToRemove);
+ }
+ else if (keyToRemove instanceof JSONArray)
+ {
+ return new PathRemover((JSONArray)keyToRemove);
+ }
+ else if (keyToRemove instanceof List>)
+ {
+ return new PathRemover((List)keyToRemove);
+ }
+ else
+ {
+ throw new IllegalArgumentException("bad test setup: wrong type of key to remove");
+ }
+ }
+
+}
diff --git a/json-smart/src/test/java/net/minidev/json/test/actions/PathReplicatorTest.java b/json-smart/src/test/java/net/minidev/json/test/actions/PathReplicatorTest.java
new file mode 100755
index 00000000..3c9c13de
--- /dev/null
+++ b/json-smart/src/test/java/net/minidev/json/test/actions/PathReplicatorTest.java
@@ -0,0 +1,232 @@
+package net.minidev.json.test.actions;
+
+import net.minidev.json.actions.PathReplicator;
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author adoneitan@gmail.com
+ */
+@RunWith(Parameterized.class)
+public class PathReplicatorTest
+{
+ private String jsonSource;
+ private Object pathsToCopy;
+ private Object expected;
+
+ public PathReplicatorTest(String jsonSource, Object pathsToCopy, Object expected)
+ {
+ this.jsonSource = jsonSource;
+ this.pathsToCopy = pathsToCopy;
+ this.expected = expected;
+ }
+
+ @Parameterized.Parameters
+ public static Collection params()
+ {
+ return Arrays.asList(new Object[][]{
+
+ //nulls, bad/empty keys
+ {null, null, null },
+ {null, "", null },
+ {null, "k1", null },
+ {null, new String[]{}, null },
+ {null, new JSONArray(), null },
+ {null, new ArrayList(0), null },//5
+
+ //empty json, bad/empty keys
+ {"{}", null, "{}" },
+ {"{}", "", "{}" },
+ {"{}", "k1", "{}" },
+ {"{}", new String[]{}, "{}" },
+ {"{}", new JSONArray(), "{}" },
+ {"{}", new ArrayList(0), "{}" },//11
+
+ //simple json, bad/empty keys
+ {"{\"k0\":\"v0\"}", null, "{}" },
+ {"{\"k0\":\"v0\"}", "", "{}" },
+ {"{\"k0\":\"v0\"}", "k1", "{}" },
+ {"{\"k0\":\"v0\"}", new String[]{}, "{}" },
+ {"{\"k0\":\"v0\"}", new JSONArray(), "{}" },
+ {"{\"k0\":\"v0\"}", new ArrayList(0), "{}" },//17
+
+ //simple json, valid/invalid keys
+ {"{\"k0\":\"v0\"}", "k0", "{\"k0\":\"v0\"}" },
+ {"{\"k0\":\"v0\"}", "v0", "{}" },
+ {"{\"k0\":\"v0\"}", "k0.k1", "{}" },//20
+ {"{\"k0\":\"v0\"}", "k1.k0", "{}" },
+ {"{\"k0\":null}", "k0", "{\"k0\":null}" },
+ {"{\"k0\":null}", "v0", "{}" },
+
+ //key with dot char
+ {"{\"k0.k1\":\"v0\"}", "k0", "{}" },
+ {"{\"k0.k1\":\"v0\"}", "k1", "{}" },
+ {"{\"k0.k1\":\"v0\"}", "k0.k1", "{}" },
+
+ // key with dot ambiguity
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k0", "{\"k0\":{}}" },
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k1", "{}" },
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k0.k1", "{\"k0\":{\"k1\":null}}" },
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k0.k1", "{\"k0\":{\"k1\":{}}}" },
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k0.k1.k2", "{\"k0\":{\"k1\":{\"k2\":null}}}" },
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k1.k2", "{}" },
+ {"{\"k0\":{\"k1.k2\":\"dot\"},\"k1\":{\"k2\":\"v2\"}}}","k0", "{\"k0\":{}}}" },
+ {"{\"k0\":{\"k1.k2\":\"dot\"},\"k1\":{\"k2\":\"v2\"}}}","k1.k2", "{\"k1\":{\"k2\":\"v2\"}}}" },
+ {"{\"k0\":{\"k1\":\"v1\",\"k2\":{\"k3.k4\":\"dot\"}}}", "k0.k2.k3.k4", "{}" },
+ {"{\"k0\":{\"k1\":\"v1\",\"k2\":{\"k3.k4\":\"dot\"}}}", "k0.k2.k3", "{}" },
+ {"{\"k0\":{\"k1\":\"v1\",\"k2\":{\"k3.k4\":\"dot\"}}}", "k0.k2", "{\"k0\":{\"k2\":{}}}" },
+ {"{\"k0\":{\"k1\":\"v1\",\"k2\":{\"k3.k4\":\"dot\"}}}", "k0", "{\"k0\":{}}" },//38
+
+ //ignore non-existent keys but keep good keys
+ {"{\"k0\":\"v0\",\"k1\":\"v1\"}", new String[]{"k0","k2"}, "{\"k0\":\"v0\"}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k2"}, "{\"k0\":\"v0\"}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1.k2"}, "{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k0.k2"}, "{\"k0\":\"v0\"}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1.k2","k1"}, "{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1"}, "{\"k0\":\"v0\",\"k1\":{}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1","k2"}, "{\"k0\":\"v0\",\"k1\":{}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k1.k2"}, "{\"k1\":{\"k2\":\"v2\"}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k1.k2.k3"}, "{}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0.k1.k2"}, "{}" },//48
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k1.k0"}, "{}" },
+
+ //arrays - key inside array treated as child
+ {"{\"k0\":{\"k1\":[1,2,3,4]}}", "k0", "{\"k0\":{}}" },
+ {"{\"k0\":{\"k1\":[1,2,3,4]}}", "k0.k1", "{\"k0\":{\"k1\":[1,2,3,4]}}" },
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0", "{\"k0\":{}}" },
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1", "{\"k0\":{\"k1\":[1,{},3,4]}}" },
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1.k2", "{\"k0\":{\"k1\":[{\"k2\":\"v2\"}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"},{\"k2\":\"v2\"}]}}", "k0.k1", "{\"k0\":{\"k1\":[{},{}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"},{\"k2\":\"v2\"}]}}", "k0.k1.k2", "{\"k0\":{\"k1\":[{\"k2\":\"v2\"},{\"k2\":\"v2\"}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"}],\"k3\":[{\"k4\":\"v4\"}]}}", "k0.k1.k2", "{\"k0\":{\"k1\":[{\"k2\":\"v2\"}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"}],\"k3\":[{\"k4\":\"v4\"}]}}", "k0.k3.k4", "{\"k0\":{\"k3\":[{\"k4\":\"v4\"}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"}],\"k3\":[{\"k4\":{\"k5\":\"v5\"}}]}}", "k0.k1.k2", "{\"k0\":{\"k1\":[{\"k2\":\"v2\"}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"}],\"k3\":[{\"k4\":{\"k5\":\"v5\"}}]}}", "k0.k3.k4", "{\"k0\":{\"k3\":[{\"k4\":{}}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"}],\"k3\":[{\"k4\":{\"k5\":\"v5\"}}]}}", "k0.k3.k4.k5", "{\"k0\":{\"k3\":[{\"k4\":{\"k5\":\"v5\"}}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"}],\"k3\":[{\"k4\":{\"k5\":\"v5\"}}]}}", new String[]{"k0.k1", "k0.k3"}, "{\"k0\":{\"k3\":[{}],\"k1\":[{}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"}],\"k3\":[{\"k4\":{\"k5\":\"v5\"}}]}}", new String[]{"k0.k1", "k0.k3.k4.k5"}, "{\"k0\":{\"k3\":[{\"k4\":{\"k5\":\"v5\"}}],\"k1\":[{}]}}" },
+ });
+ }
+
+ @Test
+ public void test() throws Exception
+ {
+ JSONObject objectSource = jsonSource != null ? (JSONObject) JSONValue.parseWithException(jsonSource) :null;
+ PathReplicator copier = switchKeyToCopy();
+ JSONObject copied = copier.replicate(objectSource);
+ JSONObject expectedObj = expected != null ? (JSONObject) JSONValue.parseWithException((String) expected) : null;
+ assertEquals(expectedObj, copied);
+ }
+
+ @Test
+ public void test2() throws Exception
+ {
+ JSONObject objectSource = jsonSource != null ? (JSONObject) JSONValue.parseWithException(jsonSource) :null;
+ PathReplicator copier = switchKeyToCopy2();
+ JSONObject copied = copier.replicate(objectSource);
+ JSONObject expectedObj = expected != null ? (JSONObject) JSONValue.parseWithException((String) expected) : null;
+ assertEquals(expectedObj, copied);
+ }
+
+ private PathReplicator switchKeyToCopy()
+ {
+ long m = System.currentTimeMillis();
+ if (pathsToCopy == null && m % 4 == 0)
+ {
+ System.out.println("cast to String");
+ return new PathReplicator((String)null);
+ }
+ else if (pathsToCopy == null && m % 4 == 1)
+ {
+ System.out.println("cast to String[]");
+ return new PathReplicator((String[])null);
+ }
+ else if (pathsToCopy == null && m % 4 == 2)
+ {
+ System.out.println("cast to JSONArray");
+ return new PathReplicator((JSONArray)null);
+ }
+ else if (pathsToCopy == null && m % 4 == 3)
+ {
+ System.out.println("cast to List");
+ return new PathReplicator((List)null);
+ }
+ else if (pathsToCopy instanceof String)
+ {
+ return new PathReplicator((String) pathsToCopy);
+ }
+ else if (pathsToCopy instanceof String[])
+ {
+ return new PathReplicator((String[]) pathsToCopy);
+ }
+ else if (pathsToCopy instanceof JSONArray)
+ {
+ return new PathReplicator((JSONArray) pathsToCopy);
+ }
+ else if (pathsToCopy instanceof List>)
+ {
+ return new PathReplicator((List) pathsToCopy);
+ }
+ else
+ {
+ throw new IllegalArgumentException("bad test setup: wrong type of key to remove");
+ }
+ }
+
+ private PathReplicator switchKeyToCopy2()
+ {
+ long m = System.currentTimeMillis();
+ if (pathsToCopy == null && m % 4 == 0)
+ {
+ System.out.println("cast to String");
+ return new PathReplicator((String)null);
+ }
+ else if (pathsToCopy == null && m % 4 == 1)
+ {
+ System.out.println("cast to String[]");
+ return new PathReplicator((String[])null);
+ }
+ else if (pathsToCopy == null && m % 4 == 2)
+ {
+ System.out.println("cast to JSONArray");
+ return new PathReplicator((JSONArray)null);
+ }
+ else if (pathsToCopy == null && m % 4 == 3)
+ {
+ System.out.println("cast to List");
+ return new PathReplicator((List)null);
+ }
+ else if (pathsToCopy instanceof String)
+ {
+ return new PathReplicator((String) pathsToCopy);
+ }
+ else if (pathsToCopy instanceof String[])
+ {
+ return new PathReplicator((String[]) pathsToCopy);
+ }
+ else if (pathsToCopy instanceof JSONArray)
+ {
+ return new PathReplicator((JSONArray) pathsToCopy);
+ }
+ else if (pathsToCopy instanceof List>)
+ {
+ return new PathReplicator((List) pathsToCopy);
+ }
+ else
+ {
+ throw new IllegalArgumentException("bad test setup: wrong type of key to remove");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/json-smart/src/test/java/net/minidev/json/test/actions/PathsRetainerTest.java b/json-smart/src/test/java/net/minidev/json/test/actions/PathsRetainerTest.java
new file mode 100755
index 00000000..17daf438
--- /dev/null
+++ b/json-smart/src/test/java/net/minidev/json/test/actions/PathsRetainerTest.java
@@ -0,0 +1,169 @@
+package net.minidev.json.test.actions;
+
+import net.minidev.json.actions.PathsRetainer;
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.minidev.json.actions.path.DotDelimiter;
+import net.minidev.json.parser.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author adoneitan@gmail.com
+ */
+@RunWith(Parameterized.class)
+public class PathsRetainerTest
+{
+ private String jsonToReduce;
+ private Object keyToKeep;
+ private String expectedReducedJson;
+
+ public PathsRetainerTest(String jsonToReduce, Object keyToKeep, String expectedReducedJson)
+ {
+ this.jsonToReduce = jsonToReduce;
+ this.keyToKeep = keyToKeep;
+ this.expectedReducedJson = expectedReducedJson;
+ }
+
+ @Parameterized.Parameters
+ public static Collection params()
+ {
+ return Arrays.asList(new Object[][]{
+
+ //nulls, bad/empty keys
+ {null, null, null },
+ {null, "", null },
+ {null, "k1", null },
+ {null, new String[]{}, null },
+ {null, new JSONArray(), null },
+ {null, new ArrayList(0), null },//5
+
+ //empty json, bad/empty keys
+ {"{}", null, "{}" },
+ {"{}", "", "{}" },
+ {"{}", "k1", "{}" },
+ {"{}", new String[]{}, "{}" },
+ {"{}", new JSONArray(), "{}" },
+ {"{}", new ArrayList(0), "{}" },//11
+
+ //simple json, bad/empty keys
+ {"{\"k0\":\"v0\"}", null, "{}" },
+ {"{\"k0\":\"v0\"}", "", "{}" },
+ {"{\"k0\":\"v0\"}", "k1", "{}" },
+ {"{\"k0\":\"v0\"}", new String[]{}, "{}" },
+ {"{\"k0\":\"v0\"}", new JSONArray(), "{}" },
+ {"{\"k0\":\"v0\"}", new ArrayList(0), "{}" },//17
+
+ //simple json, valid/invalid keys
+ {"{\"k0\":\"v0\"}", "k0", "{\"k0\":\"v0\"}" },
+ {"{\"k0\":\"v0\"}", "v0", "{}" },
+ {"{\"k0\":\"v0\"}", "k0.k1", "{}" },
+ {"{\"k0\":\"v0\"}", "k1.k0", "{}" },
+ {"{\"k0\":null}", "k0", "{\"k0\":null}" },
+ {"{\"k0\":null}", "v0", "{}" },//23
+
+ //key with dot char
+ {"{\"k0.k1\":\"v0\"}", "k0", "{}" },
+ {"{\"k0.k1\":\"v0\"}", "k1", "{}" },
+ {"{\"k0.k1\":\"v0\"}", "k0.k1", "{}" },
+
+ // key with dot ambiguity
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k0", "{\"k0\":{}}" },//27
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k1", "{}" },
+ {"{\"k0.k1\":\"withDot\",\"k0\":{\"k1\":null}}", "k0.k1", "{\"k0\":{\"k1\":null}}" },//29
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k0.k1", "{\"k0\":{\"k1\":{}}}" },
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k0.k1.k2", "{\"k0\":{\"k1\":{\"k2\":null}}}" },//31
+ {"{\"k0\":{\"k1.k2\":\"dot\",\"k1\":{\"k2\":null}}}", "k1.k2", "{}" },
+ {"{\"k0\":{\"k1.k2\":\"dot\"},\"k1\":{\"k2\":\"v2\"}}}","k0", "{\"k0\":{}}}" },
+ {"{\"k0\":{\"k1.k2\":\"dot\"},\"k1\":{\"k2\":\"v2\"}}}","k1.k2", "{\"k1\":{\"k2\":\"v2\"}}}" },
+ {"{\"k0\":{\"k1\":\"v1\",\"k2\":{\"k3.k4\":\"dot\"}}}", "k0.k2.k3.k4", "{}" },
+ {"{\"k0\":{\"k1\":\"v1\",\"k2\":{\"k3.k4\":\"dot\"}}}", "k0.k2.k3", "{}" },
+ {"{\"k0\":{\"k1\":\"v1\",\"k2\":{\"k3.k4\":\"dot\"}}}", "k0.k2", "{\"k0\":{\"k2\":{}}}" },
+ {"{\"k0\":{\"k1\":\"v1\",\"k2\":{\"k3.k4\":\"dot\"}}}", "k0", "{\"k0\":{}}" },
+
+ //ignore non-existent keys but keep good keys
+ {"{\"k0\":\"v0\",\"k1\":\"v1\"}", new String[]{"k0","k2"}, "{\"k0\":\"v0\"}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k2"}, "{\"k0\":\"v0\"}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1.k2"}, "{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k0.k2"}, "{\"k0\":\"v0\"}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1.k2","k1"}, "{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1"}, "{\"k0\":\"v0\",\"k1\":{}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0","k1","k2"}, "{\"k0\":\"v0\",\"k1\":{}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k1.k2"}, "{\"k1\":{\"k2\":\"v2\"}}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k1.k2.k3"}, "{}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k0.k1.k2"}, "{}" },
+ {"{\"k0\":\"v0\",\"k1\":{\"k2\":\"v2\"}}", new String[]{"k1.k0"}, "{}" },
+
+ //arrays - key inside array treated as child
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0", "{\"k0\":{}}" },
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1", "{\"k0\":{\"k1\":[1,{},3,4]}}" },
+ {"{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}", "k0.k1.k2", "{\"k0\":{\"k1\":[1,{\"k2\":\"v2\"},3,4]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"},{\"k2\":\"v2\"}]}}", "k0.k1", "{\"k0\":{\"k1\":[{},{}]}}" },
+ {"{\"k0\":{\"k1\":[{\"k2\":\"v2\"},{\"k2\":\"v2\"}]}}", "k0.k1.k2", "{\"k0\":{\"k1\":[{\"k2\":\"v2\"},{\"k2\":\"v2\"}]}}" },
+ });
+ }
+
+ @Test
+ public void test() throws ParseException
+ {
+ JSONObject objectToReduce = jsonToReduce != null ? (JSONObject) JSONValue.parseWithException(jsonToReduce) :null;
+ JSONObject expectedReducedObj = expectedReducedJson != null ? (JSONObject) JSONValue.parseWithException(expectedReducedJson):null;
+ PathsRetainer retainer = switchKeyToRemove().with(new DotDelimiter().withAcceptDelimiterInNodeName(false));
+ JSONObject reducedObj = retainer.retain(objectToReduce);
+ assertEquals(expectedReducedObj, reducedObj);
+ }
+
+ private PathsRetainer switchKeyToRemove()
+ {
+ long m = System.currentTimeMillis();
+ if (keyToKeep == null && m % 4 == 0)
+ {
+ System.out.println("cast to String");
+ return new PathsRetainer((String)null);
+ }
+ else if (keyToKeep == null && m % 4 == 1)
+ {
+ System.out.println("cast to String[]");
+ return new PathsRetainer((String[])null);
+ }
+ else if (keyToKeep == null && m % 4 == 2)
+ {
+ System.out.println("cast to JSONArray");
+ return new PathsRetainer((JSONArray)null);
+ }
+ else if (keyToKeep == null && m % 4 == 3)
+ {
+ System.out.println("cast to List");
+ return new PathsRetainer((List)null);
+ }
+ else if (keyToKeep instanceof String)
+ {
+ return new PathsRetainer((String) keyToKeep);
+ }
+ else if (keyToKeep instanceof String[])
+ {
+ return new PathsRetainer((String[]) keyToKeep);
+ }
+ else if (keyToKeep instanceof JSONArray)
+ {
+ return new PathsRetainer((JSONArray) keyToKeep);
+ }
+ else if (keyToKeep instanceof List>)
+ {
+ return new PathsRetainer((List) keyToKeep);
+ }
+ else
+ {
+ throw new IllegalArgumentException("bad test setup: wrong type of key to remove");
+ }
+ }
+}
\ No newline at end of file
diff --git a/json-smart/src/test/java/net/minidev/json/test/actions/TreePathTest.java b/json-smart/src/test/java/net/minidev/json/test/actions/TreePathTest.java
new file mode 100755
index 00000000..876bfb3f
--- /dev/null
+++ b/json-smart/src/test/java/net/minidev/json/test/actions/TreePathTest.java
@@ -0,0 +1,97 @@
+package net.minidev.json.test.actions;
+
+import net.minidev.json.actions.path.DotDelimiter;
+import net.minidev.json.actions.path.TreePath;
+import net.minidev.json.actions.path.PathDelimiter;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author adoneitan@gmail.com
+ */
+public class TreePathTest
+{
+ private static final PathDelimiter delim = new DotDelimiter().withAcceptDelimiterInNodeName(true);
+
+ @Test
+ public void testIterator()
+ {
+ TreePath jp = new TreePath("a.b.c", delim);
+ assertTrue(jp.nextIndex() == 0);
+ assertTrue(jp.prevIndex() == -1);
+ assertTrue("".equals(jp.curr()));
+ assertTrue("".equals(jp.origin()));
+ assertTrue("a.b.c".equals(jp.remainder()));
+ assertTrue(jp.hasNext());
+ assertFalse(jp.hasPrev());
+
+ jp.next();
+ assertTrue("a".equals(jp.curr()));
+ assertTrue("a".equals(jp.origin()));
+ assertTrue("b.c".equals(jp.remainder()));
+ assertTrue(jp.hasNext());
+ assertTrue(jp.hasPrev());
+
+ jp.next();
+ assertTrue("b".equals(jp.curr()));
+ assertTrue("a.b".equals(jp.origin()));
+ assertTrue("c".equals(jp.remainder()));
+ assertTrue(jp.hasNext());
+ assertTrue(jp.hasPrev());
+
+ jp.next();
+ assertTrue("c".equals(jp.curr()));
+ assertTrue("a.b.c".equals(jp.origin()));
+ assertTrue("".equals(jp.remainder()));
+ assertFalse(jp.hasNext());
+ assertTrue(jp.hasPrev());
+
+ /** the first prev() after a next only changes direction. see {@link ListIterator} for details */
+ jp.prev();
+ assertTrue("c".equals(jp.curr()));
+ assertTrue("a.b.c".equals(jp.origin()));
+ assertTrue("".equals(jp.remainder()));
+ assertTrue(jp.hasNext());
+ assertTrue(jp.hasPrev());
+
+ jp.prev();
+ assertTrue("b".equals(jp.curr()));
+ assertTrue("a.b".equals(jp.origin()));
+ assertTrue("c".equals(jp.remainder()));
+ assertTrue(jp.hasNext());
+ assertTrue(jp.hasPrev());
+
+ jp.prev();
+ assertTrue("a".equals(jp.curr()));
+ assertTrue("a".equals(jp.origin()));
+ assertTrue("b.c".equals(jp.remainder()));
+ assertTrue(jp.hasNext());
+ assertFalse(jp.hasPrev());
+ }
+
+ @Test
+ public void testSubPath()
+ {
+ TreePath jp = new TreePath("a.b.c", delim);
+ assertTrue(jp.subPath(1,2).equals("b.c"));
+ }
+
+ @Test
+ public void testClone() throws CloneNotSupportedException
+ {
+ TreePath jp1 = new TreePath("a.b.c", delim);
+ TreePath jp2 = jp1.clone();
+ assertTrue(jp1.equals(jp2));
+
+ jp1.next();
+ TreePath jp3 = jp1.clone();
+ assertTrue(jp1.equals(jp3));
+
+ jp1.prev();
+ TreePath jp4 = jp1.clone();
+ assertTrue(jp1.equals(jp4));
+
+ }
+}
\ No newline at end of file