diff --git a/.gitignore b/.gitignore index 286679a2..8c0b7388 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ json-smart/target/ *.classpath *.project *.prefs +*.iml +.idea \ No newline at end of file diff --git a/json-smart/pom.xml b/json-smart/pom.xml index 6f8ef200..5e35f2f1 100644 --- a/json-smart/pom.xml +++ b/json-smart/pom.xml @@ -3,7 +3,7 @@ 4.0.0 net.minidev json-smart - 2.2.1 + 2.2.1-b-SNAPSHOT JSON Small and Fast Parser JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate. It is based on a subset of the JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999. JSON is a text format that is completely language independent but uses conventions that are familiar to programmers of the C-family of languages, including C, C++, C#, Java, JavaScript, Perl, Python, and many others. These properties make JSON an ideal data-interchange language. @@ -23,6 +23,12 @@ + + erav + Eitan Raviv + adoneitan@gmail.com + GMT+2 + 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