Browse Source

Added support for splitting JSON objects using the streaming API. Only basic expressions supported.

pull/93/head
Steve White 10 years ago
parent
commit
f0844a8a41
  1. 4
      json-path/src/main/java/com/jayway/jsonpath/EvaluationCallback.java
  2. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayToken.java
  3. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/token/FloatToken.java
  4. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/token/IntToken.java
  5. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/token/ObjectToken.java
  6. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/token/StringToken.java
  7. 155
      json-path/src/main/java/com/jayway/jsonpath/internal/token/TokenStack.java
  8. 36
      json-path/src/main/java/com/jayway/jsonpath/internal/token/TokenStackElement.java
  9. 4
      json-path/src/test/java/com/jayway/jsonpath/CallbackRecorder.java
  10. 10
      json-path/src/test/java/com/jayway/jsonpath/JacksonTest.java
  11. 48
      json-path/src/test/java/com/jayway/jsonpath/JacksonTest_Split.java
  12. 4572
      json-path/src/test/resources/json_opsview1.json

4
json-path/src/main/java/com/jayway/jsonpath/EvaluationCallback.java

@ -26,12 +26,12 @@ public interface EvaluationCallback {
* Callback invoked when result is found * Callback invoked when result is found
* @param path -- the specific path that was triggered * @param path -- the specific path that was triggered
*/ */
void resultFound(Path path); public void resultFound(Object source, Object obj, Path path) throws Exception;
/** /**
* Callback invoked when the parser leaves the region in which the match * Callback invoked when the parser leaves the region in which the match
* was found * was found
* @param path -- the specific path that was untriggered * @param path -- the specific path that was untriggered
*/ */
void resultFoundExit(Path path); public void resultFoundExit(Object source, Object obj, Path path) throws Exception;
} }

2
json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayToken.java

@ -5,7 +5,7 @@ package com.jayway.jsonpath.internal.token;
* *
* @author Hunter Payne * @author Hunter Payne
**/ **/
public class ArrayToken implements TokenStackElement public class ArrayToken extends TokenStackElement
{ {
int currentIndex; int currentIndex;
TokenStackElement value; // can be an object, array, or property TokenStackElement value; // can be an object, array, or property

2
json-path/src/main/java/com/jayway/jsonpath/internal/token/FloatToken.java

@ -5,7 +5,7 @@ package com.jayway.jsonpath.internal.token;
* *
* @author Hunter Payne * @author Hunter Payne
**/ **/
public class FloatToken implements TokenStackElement public class FloatToken extends TokenStackElement
{ {
public float value; public float value;

2
json-path/src/main/java/com/jayway/jsonpath/internal/token/IntToken.java

@ -5,7 +5,7 @@ package com.jayway.jsonpath.internal.token;
* *
* @author Hunter Payne * @author Hunter Payne
**/ **/
public class IntToken implements TokenStackElement public class IntToken extends TokenStackElement
{ {
public int value; public int value;

2
json-path/src/main/java/com/jayway/jsonpath/internal/token/ObjectToken.java

@ -5,7 +5,7 @@ package com.jayway.jsonpath.internal.token;
* *
* @author Hunter Payne * @author Hunter Payne
**/ **/
public class ObjectToken implements TokenStackElement public class ObjectToken extends TokenStackElement
{ {
String key; String key;
TokenStackElement value; // can be an array, object, or property TokenStackElement value; // can be an array, object, or property

2
json-path/src/main/java/com/jayway/jsonpath/internal/token/StringToken.java

@ -5,7 +5,7 @@ package com.jayway.jsonpath.internal.token;
* *
* @author Hunter Payne * @author Hunter Payne
**/ **/
public class StringToken implements TokenStackElement public class StringToken extends TokenStackElement
{ {
public String value; public String value;

155
json-path/src/main/java/com/jayway/jsonpath/internal/token/TokenStack.java

@ -2,14 +2,15 @@
package com.jayway.jsonpath.internal.token; package com.jayway.jsonpath.internal.token;
import java.util.*; import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.EvaluationCallback; import com.jayway.jsonpath.EvaluationCallback;
import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.internal.Path;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* *
@ -17,10 +18,11 @@ import com.fasterxml.jackson.core.JsonToken;
**/ **/
public class TokenStack public class TokenStack
{ {
protected static Logger log = Logger.getLogger("com.jayway.jsonpath"); private static final Logger log = LoggerFactory.getLogger(TokenStack.class);
protected Configuration conf; protected Configuration conf;
protected Stack<TokenStackElement> elements; protected Stack<TokenStackElement> elements;
protected Stack<Object> objStack;
protected List<Path> paths; protected List<Path> paths;
protected Map<TokenStackElement, Path> matchedPaths; protected Map<TokenStackElement, Path> matchedPaths;
@ -33,6 +35,7 @@ public class TokenStack
paths = new ArrayList<Path>(); paths = new ArrayList<Path>();
matchedPaths = new HashMap<TokenStackElement, Path>(); matchedPaths = new HashMap<TokenStackElement, Path>();
elements = new Stack<TokenStackElement>(); elements = new Stack<TokenStackElement>();
objStack = new Stack<Object>();
rootMatch = null; rootMatch = null;
} }
@ -49,14 +52,21 @@ public class TokenStack
paths.add(path); paths.add(path);
} }
public void read(JsonParser parser, EvaluationCallback callback)
throws Exception
{
read(parser,callback,true);
}
/** /**
* reads from stream and notifies the callback of matched registered paths * reads from stream and notifies the callback of matched registered paths
*/ */
public void read(JsonParser parser, EvaluationCallback callback) public void read(JsonParser parser, EvaluationCallback callback, boolean getParents)
throws Exception throws Exception
{ {
assert(callback != null); assert(callback != null);
Object obj = null;
boolean needsPathCheck = false; boolean needsPathCheck = false;
/* /*
if (null == curr && elements.empty()) { if (null == curr && elements.empty()) {
@ -71,6 +81,7 @@ public class TokenStack
} }
*/ */
while (parser.nextToken() != null) { while (parser.nextToken() != null) {
//log.debug("type/name/val: " + parser.getCurrentToken() + " " + parser.getCurrentName() + " " + parser.getText());
boolean saveMatch = false; boolean saveMatch = false;
switch (parser.getCurrentToken()) { switch (parser.getCurrentToken()) {
case START_ARRAY: case START_ARRAY:
@ -85,33 +96,42 @@ public class TokenStack
saveMatch = true; saveMatch = true;
needsPathCheck = true; needsPathCheck = true;
elements.push(curr); elements.push(curr);
obj = stackPush(parser.getCurrentName(),new JSONArray());
break; break;
} }
case END_ARRAY: case END_ARRAY:
{ {
Path match = matchedPaths.remove(curr); Path match = matchedPaths.remove(curr);
if (match != null) { if (match != null) {
callback.resultFoundExit(match); callback.resultFoundExit(parser.getCurrentToken(), obj, match);
} }
elements.pop(); elements.pop();
if (elements.empty()) curr = null; if (elements.empty()) curr = null;
else curr = elements.peek(); else curr = elements.peek();
obj = stackPop(callback, curr, JSONArray.class, match);
break; break;
} }
case VALUE_EMBEDDED_OBJECT: case VALUE_EMBEDDED_OBJECT:
case START_OBJECT: case START_OBJECT:
{ {
if (curr != null && curr.getType() == TokenType.ARRAY_TOKEN) { obj = stackPush(parser.getCurrentName(), new JSONObject());
if (isArray(curr)) {
if (((ArrayToken)curr).getValue() != null && if (((ArrayToken)curr).getValue() != null &&
matchedPaths.containsKey(curr)) matchedPaths.containsKey(curr))
{ {
Path match = matchedPaths.remove(curr); Path match = matchedPaths.remove(curr);
callback.resultFoundExit(match); if (getParents) {
callback.resultFoundExit(parser.getCurrentToken(), obj, match);
}
if (match.checkForMatch(this)) { if (match.checkForMatch(this)) {
matchedPaths.put(curr, match); matchedPaths.put(curr, match);
callback.resultFound(match); //callback.resultFound(match);
curr.setMatched();
} }
} }
} else if (null == curr && elements.empty()) { } else if (null == curr && elements.empty()) {
@ -119,7 +139,7 @@ public class TokenStack
for (Path path : paths) { for (Path path : paths) {
if (path.checkForMatch(this)) { if (path.checkForMatch(this)) {
matchedPaths.put(curr, path); matchedPaths.put(curr, path);
callback.resultFound(path); //callback.resultFound(path);
rootMatch = path; rootMatch = path;
} }
} }
@ -139,20 +159,24 @@ public class TokenStack
} }
case END_OBJECT: case END_OBJECT:
{ {
if (getParents) {
if (!"$".equals(curr)) { if (!"$".equals(curr)) {
Path match = matchedPaths.remove(curr); Path match = matchedPaths.remove(curr);
if (match != null) { if (match != null) {
callback.resultFoundExit(match); callback.resultFoundExit(parser.getCurrentToken(), obj, match);
} }
} else { } else {
Path match = matchedPaths.get("$"); Path match = matchedPaths.get("$");
if (match != null) { if (match != null) {
callback.resultFoundExit(match); callback.resultFoundExit(parser.getCurrentToken(), obj, match);
} }
} }
}
elements.pop(); elements.pop();
if (elements.empty()) curr = null; if (elements.empty()) curr = null;
else curr = elements.peek(); else curr = elements.peek();
obj = stackPop(callback, curr, JSONObject.class, null);
break; break;
} }
case FIELD_NAME: case FIELD_NAME:
@ -166,6 +190,7 @@ public class TokenStack
StringToken newToken = new StringToken("FALSE"); StringToken newToken = new StringToken("FALSE");
curr.setValue(newToken); curr.setValue(newToken);
needsPathCheck = true; needsPathCheck = true;
objPutVal(obj, curr, newToken.value);
break; break;
} }
case VALUE_TRUE: case VALUE_TRUE:
@ -173,6 +198,7 @@ public class TokenStack
StringToken newToken = new StringToken("TRUE"); StringToken newToken = new StringToken("TRUE");
curr.setValue(newToken); curr.setValue(newToken);
needsPathCheck = true; needsPathCheck = true;
objPutVal(obj, curr, newToken.value);
break; break;
} }
case VALUE_NUMBER_FLOAT: case VALUE_NUMBER_FLOAT:
@ -181,6 +207,7 @@ public class TokenStack
new FloatToken((float)parser.getValueAsDouble()); new FloatToken((float)parser.getValueAsDouble());
curr.setValue(newToken); curr.setValue(newToken);
needsPathCheck = true; needsPathCheck = true;
objPutVal(obj, curr, newToken.value);
break; break;
} }
case VALUE_NUMBER_INT: case VALUE_NUMBER_INT:
@ -188,6 +215,7 @@ public class TokenStack
IntToken newToken = new IntToken(parser.getValueAsInt()); IntToken newToken = new IntToken(parser.getValueAsInt());
curr.setValue(newToken); curr.setValue(newToken);
needsPathCheck = true; needsPathCheck = true;
objPutVal(obj, curr, newToken.value);
break; break;
} }
case VALUE_STRING: case VALUE_STRING:
@ -195,12 +223,14 @@ public class TokenStack
StringToken newToken = new StringToken(parser.getText()); StringToken newToken = new StringToken(parser.getText());
curr.setValue(newToken); curr.setValue(newToken);
needsPathCheck = true; needsPathCheck = true;
objPutVal(obj, curr, parser.getText());
break; break;
} }
case VALUE_NULL: case VALUE_NULL:
{ {
curr.setValue(null); curr.setValue(null);
needsPathCheck = true; needsPathCheck = true;
objPutVal(obj, curr, null);
break; break;
} }
default: default:
@ -211,18 +241,113 @@ public class TokenStack
for (Path path : paths) { for (Path path : paths) {
if (path.checkForMatch(this)) { if (path.checkForMatch(this)) {
if (saveMatch) matchedPaths.put(curr, path); if (saveMatch) matchedPaths.put(curr, path);
callback.resultFound(path); curr.setMatched();
if (getParents) {
callback.resultFound(parser.getCurrentToken(), obj, path);
}
} }
} }
needsPathCheck = false; needsPathCheck = false;
} }
if (rootMatch != null && elements.empty()) { if (rootMatch != null && elements.empty() && getParents) {
callback.resultFoundExit(rootMatch); if (isArray(curr)) {
obj = new JSONArray();
}
callback.resultFoundExit(parser.getCurrentToken(), obj, rootMatch);
rootMatch = null; rootMatch = null;
} }
} }
} }
private Object stackPush(String key, Object jsObj) throws Exception {
if (jsObj == null) {
return null;
}
if (this.objStack.size() > 0) {
Object obj = this.objStack.peek();
//log.trace("PP : Push[" + this.objStack.size() + "] " + jsObj.getClass().getSimpleName() + " -> " + obj.getClass());
if (obj.getClass() == JSONArray.class) {
((JSONArray)obj).add(jsObj);
} else if (obj.getClass() == JSONObject.class) {
((JSONObject)obj).put(key, jsObj);
} else {
throw new Exception("Unhandled type: " + obj.getClass());
}
} else {
//log.trace("PP : Push " + jsObj.getClass().getSimpleName() + " -> ROOT");
}
this.objStack.add(jsObj);
return jsObj;
}
private <T> Object stackPop(EvaluationCallback callback, TokenStackElement tse, T jsObj, Path path) throws Exception {
if (jsObj == null) {
return null;
}
Object obj = null;
Object popObj = null;
if (this.objStack.size() > 0) {
popObj = this.objStack.peek();
//log.trace("PP : Pop " +( popObj != null ? popObj.getClass() : " null"));
if (popObj.getClass() != jsObj) {
throw new Exception("Unexpected type : " + popObj.getClass());
} else {
popObj = this.objStack.pop();
if (this.objStack.size() > 0) {
obj = objStack.peek();
} else {
//log.debug("PP : Now ROOT");
obj = null;
}
}
}
if (tse != null && tse.getMatched()) {
callback.resultFound("Stack", popObj, path);
}
//log.info("PP : Parent now[" + this.objStack.size() + "]: " + objShow(obj));
return obj;
}
private boolean isArray(TokenStackElement current) {
return current != null && current.getType() == TokenType.ARRAY_TOKEN;
}
public String objShow(Object obj) {
if (obj != null) {
Class cls = obj.getClass();
if (cls == JSONObject.class) {
return "JSONObject: " + ((JSONObject)obj).toString();
} else if (cls == JSONArray.class) {
return "JSONArray: " + Arrays.toString(((JSONArray)obj).toArray());
}
}
return "NA";
}
private void objPutVal(Object objIn, TokenStackElement el, Object value) throws Exception {
if (objIn == null) {
return;
}
Class objInCls = objIn.getClass();
if (objInCls == JSONObject.class) {
JSONObject obj = (JSONObject)objIn;
obj.put(((ObjectToken)el).key, value);
} else if (objInCls == JSONArray.class) {
JSONArray obj = (JSONArray)objIn;
obj.add(value);
} else {
throw new Exception("Unhandled type: " + objInCls);
}
}
public String toString() public String toString()
{ {

36
json-path/src/main/java/com/jayway/jsonpath/internal/token/TokenStackElement.java

@ -1,17 +1,45 @@
package com.jayway.jsonpath.internal.token; package com.jayway.jsonpath.internal.token;
import java.util.logging.Logger;
/** /**
* *
* @author Hunter Payne * @author Hunter Payne
**/ **/
public interface TokenStackElement public abstract class TokenStackElement
{ {
public TokenType getType(); // otherwise its an object private static Logger log = Logger.getLogger(TokenStackElement.class.getName());
private boolean matched = false;
private TokenStackElement parent;
public abstract TokenType getType(); // otherwise its an object
public abstract TokenStackElement getValue();
public abstract void setValue(TokenStackElement elem);
public TokenStackElement getParent() {
if (parent == null) {
return this;
}
//log.trace("parent: " + parent);
return parent;
}
public TokenStackElement getValue(); public void setParent(TokenStackElement parent) {
this.parent = parent;
}
public void setValue(TokenStackElement elem); public void setMatched() {
this.matched = true;
}
public boolean getMatched() {
return matched;
}
} }
// End TokenStackElement.java // End TokenStackElement.java

4
json-path/src/test/java/com/jayway/jsonpath/CallbackRecorder.java

@ -44,12 +44,12 @@ public class CallbackRecorder implements EvaluationCallback {
results = new ArrayList<CallbackEvent>(); results = new ArrayList<CallbackEvent>();
} }
public void resultFound(Path path) { public void resultFound(Object src, Object val, Path path) {
System.err.println("found result " + path); System.err.println("found result " + path);
results.add(new CallbackEvent(path, false)); results.add(new CallbackEvent(path, false));
} }
public void resultFoundExit(Path path) { public void resultFoundExit(Object src, Object val, Path path) {
System.err.println("exiting result " + path); System.err.println("exiting result " + path);
results.add(new CallbackEvent(path, true)); results.add(new CallbackEvent(path, true));
} }

10
json-path/src/test/java/com/jayway/jsonpath/JacksonTest.java

@ -9,12 +9,10 @@ import java.nio.charset.Charset;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonFactory;
import com.jayway.jsonpath.EvaluationCallback; import com.jayway.jsonpath.EvaluationCallback;
import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.internal.Path;
import com.jayway.jsonpath.internal.PathCompiler; import com.jayway.jsonpath.internal.PathCompiler;
import com.jayway.jsonpath.internal.token.*; import com.jayway.jsonpath.internal.token.*;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -607,7 +605,7 @@ public class JacksonTest extends BaseTest implements EvaluationCallback {
equals(new CallbackRecorder.CallbackEvent(rootPath, true))); equals(new CallbackRecorder.CallbackEvent(rootPath, true)));
} }
public void resultFound(Path path) { public void resultFound(Object src, Object val, Path path) {
if (path == idPath) { if (path == idPath) {
switch (match++) { switch (match++) {
case 0: case 0:
@ -1040,14 +1038,14 @@ public class JacksonTest extends BaseTest implements EvaluationCallback {
break; break;
} }
} }
recorder.resultFound(path); recorder.resultFound(src, val, path);
} }
public void resultFoundExit(Path path) { public void resultFoundExit(Object src, Object val, Path path) {
assert(path != idPath); assert(path != idPath);
assert(path != floatPath); assert(path != floatPath);
assert(path != intPath); assert(path != intPath);
recorder.resultFoundExit(path); recorder.resultFoundExit(src, val, path);
} }
protected void checkResult(TokenStack stack, Object expected) { protected void checkResult(TokenStack stack, Object expected) {

48
json-path/src/test/java/com/jayway/jsonpath/JacksonTest_Split.java

@ -0,0 +1,48 @@
package com.jayway.jsonpath;
import static org.junit.Assert.*;
import com.fasterxml.jackson.core.JsonFactory;
import com.jayway.jsonpath.internal.Path;
import com.jayway.jsonpath.internal.PathCompiler;
import com.jayway.jsonpath.internal.token.TokenStack;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;
public class JacksonTest_Split extends BaseTest implements EvaluationCallback {
private static final Logger log = LoggerFactory.getLogger(JacksonTest_Split.class);
private List<Object> results = new ArrayList<Object>();
@Test
public void jsonTest() throws Exception {
String res = "json_opsview1.json";
try (InputStream stream = getClass().getClassLoader().getResourceAsStream(res)) {
Path path = PathCompiler.compile("$.list[*]");
TokenStack stack = new TokenStack(JACKSON_CONFIGURATION);
JsonFactory factory = new JsonFactory();
stack.registerPath(path);
stack.read(factory.createParser(stream), this, false);
}
log.debug("results: " + results.size());
assertTrue(results.size() == 96);
}
@Override
public void resultFound(Object source, Object obj, Path path) throws Exception {
//log.debug(source + ":" + String.valueOf(obj));
results.add(obj);
}
@Override
public void resultFoundExit(Object source, Object obj, Path path) throws Exception {
//log.debug(source + ":" + String.valueOf(obj));
}
}

4572
json-path/src/test/resources/json_opsview1.json

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save