Browse Source

Fixed issues in Filter serialization.

pull/183/merge
Kalle Stenflo 9 years ago
parent
commit
0feb2bcb88
  1. 3
      json-path/src/main/java/com/jayway/jsonpath/Filter.java
  2. 459
      json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java
  3. 12
      json-path/src/main/java/com/jayway/jsonpath/internal/filter/ExpressionNode.java
  4. 39
      json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java
  5. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalExpressionNode.java
  6. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java
  7. 75
      json-path/src/test/java/com/jayway/jsonpath/FilterCompilerTest.java
  8. 17
      json-path/src/test/java/com/jayway/jsonpath/internal/UtilsTest.java

3
json-path/src/main/java/com/jayway/jsonpath/Filter.java

@ -178,8 +178,7 @@ public abstract class Filter implements Predicate {
public static Filter parse(String filter){
Predicate f = FilterCompiler.compile(filter);
return new SingleFilter(f);
return FilterCompiler.compile(filter);
}

459
json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java

@ -15,54 +15,14 @@
package com.jayway.jsonpath.internal;
import com.jayway.jsonpath.JsonPathException;
import com.jayway.jsonpath.ValueCompareException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public final class Utils {
public static final String CR = System.getProperty("line.separator");
private static final char BACKSLASH = '\\';
/**
* Creates a range of integers from start (inclusive) to end (exclusive)
*
* @param start
* @param end
* @return
*/
public static List<Integer> createRange(int start, int end) {
if (end <= start) {
throw new IllegalArgumentException("Cannot create range from " + start + " to " + end + ", end must be greater than start.");
}
if (start == end - 1) {
return Collections.emptyList();
}
List<Integer> range = new ArrayList<Integer>(end - start - 1);
for (int i = start; i < end; i++) {
range.add(i);
}
return range;
}
public static <T> Iterable<T> reverse(final List<T> list) {
return new ListReverser<T>(list);
}
// accept a collection of objects, since all objects have toString()
public static String join(String delimiter, String wrap, Iterable<? extends Object> objs) {
Iterator<? extends Object> iter = objs.iterator();
@ -133,81 +93,166 @@ public final class Utils {
}
}
//---------------------------------------------------------
//
// Strings
//
//---------------------------------------------------------
public static boolean isInt(String str) {
public static String escape(String str, boolean escapeSingleQuote) {
if (str == null) {
return false;
return null;
}
int sz = str.length();
for (int i = 0; i < sz; i++) {
if (Character.isDigit(str.charAt(i)) == false) {
return false;
int len = str.length();
StringWriter writer = new StringWriter(len * 2);
for (int i = 0; i < len; i++) {
char ch = str.charAt(i);
// handle unicode
if (ch > 0xfff) {
writer.write("\\u" + hex(ch));
} else if (ch > 0xff) {
writer.write("\\u0" + hex(ch));
} else if (ch > 0x7f) {
writer.write("\\u00" + hex(ch));
} else if (ch < 32) {
switch (ch) {
case '\b':
writer.write('\\');
writer.write('b');
break;
case '\n':
writer.write('\\');
writer.write('n');
break;
case '\t':
writer.write('\\');
writer.write('t');
break;
case '\f':
writer.write('\\');
writer.write('f');
break;
case '\r':
writer.write('\\');
writer.write('r');
break;
default :
if (ch > 0xf) {
writer.write("\\u00" + hex(ch));
} else {
writer.write("\\u000" + hex(ch));
}
break;
}
} else {
switch (ch) {
case '\'':
if (escapeSingleQuote) {
writer.write('\\');
}
writer.write('\'');
break;
case '"':
writer.write('\\');
writer.write('"');
break;
case '\\':
writer.write('\\');
writer.write('\\');
break;
case '/':
writer.write('\\');
writer.write('/');
break;
default :
writer.write(ch);
break;
}
}
}
return true;
return writer.toString();
}
public static boolean isNumeric(String str) {
if (str == null || str.trim().isEmpty()) {
return false;
public static String unescape(String str) {
if (str == null) {
return null;
}
int sz = str.length();
for (int i = 0; i < sz; i++) {
if (Character.isDigit(str.charAt(i)) == false && !(str.charAt(i) == '.')) {
return false;
int len = str.length();
StringWriter writer = new StringWriter(len);
StringBuffer unicode = new StringBuffer(4);
boolean hadSlash = false;
boolean inUnicode = false;
for (int i = 0; i < len; i++) {
char ch = str.charAt(i);
if (inUnicode) {
unicode.append(ch);
if (unicode.length() == 4) {
try {
int value = Integer.parseInt(unicode.toString(), 16);
writer.write((char) value);
unicode.setLength(0);
inUnicode = false;
hadSlash = false;
} catch (NumberFormatException nfe) {
throw new JsonPathException("Unable to parse unicode value: " + unicode, nfe);
}
}
continue;
}
}
return true;
}
public static String unescape(String s) {
if (s.indexOf(BACKSLASH) == -1) {
return s;
}
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == BACKSLASH) {
char c2 = s.charAt(++i);
switch (c2) {
if (hadSlash) {
hadSlash = false;
switch (ch) {
case '\\':
c2 = '\\';
break;
case 'b':
c2 = '\b';
writer.write('\\');
break;
case 'f':
c2 = '\f';
case '\'':
writer.write('\'');
break;
case 'n':
c2 = '\n';
case '\"':
writer.write('"');
break;
case 'r':
c2 = '\r';
writer.write('\r');
break;
case 'f':
writer.write('\f');
break;
case 't':
c2 = '\t';
writer.write('\t');
break;
case 'n':
writer.write('\n');
break;
case 'b':
writer.write('\b');
break;
case 'u':
try {
String hex = s.substring(i + 1, i + 5);
c2 = (char) Integer.parseInt(hex, 16);
i += 4;
} catch (Exception e) {
throw new ValueCompareException("\\u parse failed", e);
}
{
inUnicode = true;
break;
}
default :
writer.write(ch);
break;
}
sb.append(c2);
} else {
sb.append(c);
continue;
} else if (ch == '\\') {
hadSlash = true;
continue;
}
writer.write(ch);
}
return sb.toString();
if (hadSlash) {
writer.write('\\');
}
return writer.toString();
}
/**
* Returns an upper case hexadecimal <code>String</code> for the given
* character.
*
* @param ch The character to convert.
* @return An upper case hexadecimal <code>String</code>
*/
public static String hex(char ch) {
return Integer.toHexString(ch).toUpperCase();
}
/**
@ -245,38 +290,7 @@ public final class Utils {
return cs.toString().indexOf(searchChar.toString(), start);
}
/**
* <p>Counts how many times the substring appears in the larger string.</p>
* <p/>
* <p>A {@code null} or empty ("") String input returns {@code 0}.</p>
* <p/>
* <pre>
* StringUtils.countMatches(null, *) = 0
* StringUtils.countMatches("", *) = 0
* StringUtils.countMatches("abba", null) = 0
* StringUtils.countMatches("abba", "") = 0
* StringUtils.countMatches("abba", "a") = 2
* StringUtils.countMatches("abba", "ab") = 1
* StringUtils.countMatches("abba", "xxx") = 0
* </pre>
*
* @param str the CharSequence to check, may be null
* @param sub the substring to count, may be null
* @return the number of occurrences, 0 if either CharSequence is {@code null}
* @since 3.0 Changed signature from countMatches(String, String) to countMatches(CharSequence, CharSequence)
*/
public static int countMatches(CharSequence str, CharSequence sub) {
if (isEmpty(str) || isEmpty(sub)) {
return 0;
}
int count = 0;
int idx = 0;
while ((idx = indexOf(str, sub, idx)) != -1) {
count++;
idx += sub.length();
}
return count;
}
//---------------------------------------------------------
//
@ -384,200 +398,9 @@ public final class Utils {
if (null == o) {
return null;
}
return o.toString();
}
//---------------------------------------------------------
//
// Serialization
//
//---------------------------------------------------------
/**
* <p>Serializes an {@code Object} to the specified stream.</p>
* <p/>
* <p>The stream will be closed once the object is written.
* This avoids the need for a finally clause, and maybe also exception
* handling, in the application code.</p>
* <p/>
* <p>The stream passed in is not buffered internally within this method.
* This is the responsibility of your application if desired.</p>
*
* @param obj the object to serialize to bytes, may be null
* @param outputStream the stream to write to, must not be null
* @throws IllegalArgumentException if {@code outputStream} is {@code null}
* @throws RuntimeException (runtime) if the serialization fails
*/
public static void serialize(Serializable obj, OutputStream outputStream) {
if (outputStream == null) {
throw new IllegalArgumentException("The OutputStream must not be null");
}
ObjectOutputStream out = null;
try {
// stream closed in the finally
out = new ObjectOutputStream(outputStream);
out.writeObject(obj);
} catch (IOException ex) {
throw new JsonPathException(ex);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException ex) { // NOPMD
// ignore close exception
}
}
}
/**
* <p>Serializes an {@code Object} to a byte array for
* storage/serialization.</p>
*
* @param obj the object to serialize to bytes
* @return a byte[] with the converted Serializable
* @throws RuntimeException (runtime) if the serialization fails
*/
public static byte[] serialize(Serializable obj) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
serialize(obj, baos);
return baos.toByteArray();
}
// Deserialize
//-----------------------------------------------------------------------
/**
* <p>Deserializes an {@code Object} from the specified stream.</p>
* <p/>
* <p>The stream will be closed once the object is written. This
* avoids the need for a finally clause, and maybe also exception
* handling, in the application code.</p>
* <p/>
* <p>The stream passed in is not buffered internally within this method.
* This is the responsibility of your application if desired.</p>
*
* @param inputStream the serialized object input stream, must not be null
* @return the deserialized object
* @throws IllegalArgumentException if {@code inputStream} is {@code null}
* @throws RuntimeException (runtime) if the serialization fails
*/
public static Object deserialize(InputStream inputStream) {
if (inputStream == null) {
throw new IllegalArgumentException("The InputStream must not be null");
}
ObjectInputStream in = null;
try {
// stream closed in the finally
in = new ObjectInputStream(inputStream);
return in.readObject();
} catch (ClassNotFoundException ex) {
throw new JsonPathException(ex);
} catch (IOException ex) {
throw new JsonPathException(ex);
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException ex) { // NOPMD
// ignore close exception
}
}
}
/**
* <p>Deserializes a single {@code Object} from an array of bytes.</p>
*
* @param objectData the serialized object, must not be null
* @return the deserialized object
* @throws IllegalArgumentException if {@code objectData} is {@code null}
* @throws RuntimeException (runtime) if the serialization fails
*/
public static Object deserialize(byte[] objectData) {
if (objectData == null) {
throw new IllegalArgumentException("The byte[] must not be null");
}
ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
return deserialize(bais);
}
/**
* <p>Custom specialization of the standard JDK {@link java.io.ObjectInputStream}
* that uses a custom <code>ClassLoader</code> to resolve a class.
* If the specified <code>ClassLoader</code> is not able to resolve the class,
* the context classloader of the current thread will be used.
* This way, the standard deserialization work also in web-application
* containers and application servers, no matter in which of the
* <code>ClassLoader</code> the particular class that encapsulates
* serialization/deserialization lives. </p>
* <p/>
* <p>For more in-depth information about the problem for which this
* class here is a workaround, see the JIRA issue LANG-626. </p>
*/
static class ClassLoaderAwareObjectInputStream extends ObjectInputStream {
private ClassLoader classLoader;
/**
* Constructor.
*
* @param in The <code>InputStream</code>.
* @param classLoader classloader to use
* @throws IOException if an I/O error occurs while reading stream header.
* @see java.io.ObjectInputStream
*/
public ClassLoaderAwareObjectInputStream(InputStream in, ClassLoader classLoader) throws IOException {
super(in);
this.classLoader = classLoader;
}
/**
* Overriden version that uses the parametrized <code>ClassLoader</code> or the <code>ClassLoader</code>
* of the current <code>Thread</code> to resolve the class.
*
* @param desc An instance of class <code>ObjectStreamClass</code>.
* @return A <code>Class</code> object corresponding to <code>desc</code>.
* @throws IOException Any of the usual Input/Output exceptions.
* @throws ClassNotFoundException If class of a serialized object cannot be found.
*/
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();
try {
return Class.forName(name, false, classLoader);
} catch (ClassNotFoundException ex) {
return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
}
}
}
private static class ListReverser<T> implements Iterable<T> {
private ListIterator<T> listIterator;
public ListReverser(List<T> wrappedList) {
this.listIterator = wrappedList.listIterator(wrappedList.size());
}
public Iterator<T> iterator() {
return new Iterator<T>() {
public boolean hasNext() {
return listIterator.hasPrevious();
}
public T next() {
return listIterator.previous();
}
public void remove() {
listIterator.remove();
}
};
}
}
private Utils() {
}
}

12
json-path/src/main/java/com/jayway/jsonpath/internal/filter/ExpressionNode.java

@ -6,16 +6,16 @@ public abstract class ExpressionNode implements Predicate {
public static ExpressionNode createExpressionNode(ExpressionNode right, LogicalOperator operator, ExpressionNode left){
if(operator == LogicalOperator.AND){
if((left instanceof LogicalExpressionNode) && ((LogicalExpressionNode)left).getOperator() == LogicalOperator.AND ){
LogicalExpressionNode len = (LogicalExpressionNode) left;
return len.append(right);
if((right instanceof LogicalExpressionNode) && ((LogicalExpressionNode)right).getOperator() == LogicalOperator.AND ){
LogicalExpressionNode len = (LogicalExpressionNode) right;
return len.append(left);
} else {
return LogicalExpressionNode.createLogicalAnd(left, right);
}
} else {
if((left instanceof LogicalExpressionNode) && ((LogicalExpressionNode)left).getOperator() == LogicalOperator.OR ){
LogicalExpressionNode len = (LogicalExpressionNode) left;
return len.append(right);
if((right instanceof LogicalExpressionNode) && ((LogicalExpressionNode)right).getOperator() == LogicalOperator.OR ){
LogicalExpressionNode len = (LogicalExpressionNode) right;
return len.append(left);
} else {
return LogicalExpressionNode.createLogicalOr(left, right);
}

39
json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java

@ -1,5 +1,6 @@
package com.jayway.jsonpath.internal.filter;
import com.jayway.jsonpath.Filter;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.Predicate;
import com.jayway.jsonpath.internal.CharacterIndex;
@ -38,9 +39,9 @@ public class FilterCompiler {
private CharacterIndex filter;
public static Predicate compile(String filterString) {
public static Filter compile(String filterString) {
FilterCompiler compiler = new FilterCompiler(filterString);
return compiler.compile();
return new CompiledFilter(compiler.compile());
}
private FilterCompiler(String filterString) {
@ -78,9 +79,15 @@ public class FilterCompiler {
case CLOSE_BRACKET:
unbalancedBrackets--;
filter.incrementPosition(1);
while(!opsStack.isEmpty()){
expStack.push(ExpressionNode.createExpressionNode(expStack.pop(), opsStack.pop(), expStack.pop()));
ExpressionNode expressionNode = expStack.pop();
if(!opsStack.isEmpty()){
ExpressionNode right = expStack.pop();
expressionNode = ExpressionNode.createExpressionNode(expressionNode, opsStack.pop(), right);
while(!opsStack.isEmpty()){
expressionNode = ExpressionNode.createExpressionNode(expressionNode, opsStack.pop(), expStack.pop());
}
}
expStack.push(expressionNode);
break;
case BANG:
filter.incrementPosition(1);
@ -336,4 +343,28 @@ public class FilterCompiler {
private boolean isRelationalOperatorChar(char c) {
return c == LT || c == GT || c == EQ || c == TILDE || c == BANG;
}
private static final class CompiledFilter extends Filter {
private final Predicate predicate;
private CompiledFilter(Predicate predicate) {
this.predicate = predicate;
}
@Override
public boolean apply(Predicate.PredicateContext ctx) {
return predicate.apply(ctx);
}
@Override
public String toString() {
String predicateString = predicate.toString();
if(predicateString.startsWith("(")){
return "[?" + predicateString + "]";
} else {
return "[?(" + predicateString + ")]";
}
}
}
}

2
json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalExpressionNode.java

@ -36,7 +36,7 @@ public class LogicalExpressionNode extends ExpressionNode {
}
public LogicalExpressionNode append(ExpressionNode expressionNode) {
chain.add(expressionNode);
chain.add(0, expressionNode);
return this;
}

2
json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java

@ -415,7 +415,7 @@ public abstract class ValueNode {
@Override
public String toString() {
return "'" + string + "'";
return "'" + Utils.escape(string, true) + "'";
}
@Override

75
json-path/src/test/java/com/jayway/jsonpath/FilterCompilerTest.java

@ -1,54 +1,41 @@
package com.jayway.jsonpath;
import com.jayway.jsonpath.internal.filter.FilterCompiler;
import org.junit.Test;
import static com.jayway.jsonpath.internal.filter.FilterCompiler.compile;
import static org.assertj.core.api.Assertions.assertThat;
public class FilterCompilerTest {
@Test
public void filter_compiler_test() {
FilterCompiler.compile("[?(@)]");
FilterCompiler.compile("[?($)]");
FilterCompiler.compile("[?(@.firstname)]");
FilterCompiler.compile("[?(@.firstname)]");
FilterCompiler.compile("[?($.firstname)]");
FilterCompiler.compile("[?(@['firstname'])]");
FilterCompiler.compile("[?($['firstname'].lastname)]");
FilterCompiler.compile("[?($['firstname']['lastname'])]");
FilterCompiler.compile("[?($['firstname']['lastname'].*)]");
FilterCompiler.compile("[?($['firstname']['num_eq'] == 1)]");
FilterCompiler.compile("[?($['firstname']['num_gt'] > 1.1)]");
FilterCompiler.compile("[?($['firstname']['num_lt'] < 11.11)]");
FilterCompiler.compile("[?($['firstname']['str_eq'] == 'hej')]");
FilterCompiler.compile("[?($['firstname']['str_eq'] == '')]");
FilterCompiler.compile("[?($['firstname']['str_eq'] == null)]");
FilterCompiler.compile("[?($['firstname']['str_eq'] == true)]");
FilterCompiler.compile("[?($['firstname']['str_eq'] == false)]");
FilterCompiler.compile("[?(@.firstname && @.lastname)]");
FilterCompiler.compile("[?((@.firstname || @.lastname) && @.and)]");
FilterCompiler.compile("[?((@.a || @.b || @.c) && @.x)]");
FilterCompiler.compile("[?((@.a && @.b && @.c) || @.x)]");
FilterCompiler.compile("[?((@.a && @.b || @.c) || @.x)]");
FilterCompiler.compile("[?((@.a && @.b) || (@.c && @.d))]");
FilterCompiler.compile("[?(@.a IN [1,2,3])]");
FilterCompiler.compile("[?(@.a IN {'foo':'bar'})]");
FilterCompiler.compile("[?(@.value<'7')]");
FilterCompiler.compile("[?(@.message == 'it\\\\')]");
assertThat(compile("[?(@)]").toString()).isEqualTo("[?(@)]");
assertThat(compile("[?(@)]").toString()).isEqualTo("[?(@)]");
assertThat(compile("[?(@.firstname)]").toString()).isEqualTo("[?(@['firstname'])]");
assertThat(compile("[?($.firstname)]").toString()).isEqualTo("[?($['firstname'])]");
assertThat(compile("[?(@['firstname'])]").toString()).isEqualTo("[?(@['firstname'])]");
assertThat(compile("[?($['firstname'].lastname)]").toString()).isEqualTo("[?($['firstname']['lastname'])]");
assertThat(compile("[?($['firstname']['lastname'])]").toString()).isEqualTo("[?($['firstname']['lastname'])]");
assertThat(compile("[?($['firstname']['lastname'].*)]").toString()).isEqualTo("[?($['firstname']['lastname'][*])]");
assertThat(compile("[?($['firstname']['num_eq'] == 1)]").toString()).isEqualTo("[?($['firstname']['num_eq'] == 1)]");
assertThat(compile("[?($['firstname']['num_gt'] > 1.1)]").toString()).isEqualTo("[?($['firstname']['num_gt'] > 1.1)]");
assertThat(compile("[?($['firstname']['num_lt'] < 11.11)]").toString()).isEqualTo("[?($['firstname']['num_lt'] < 11.11)]");
assertThat(compile("[?($['firstname']['str_eq'] == 'hej')]").toString()).isEqualTo("[?($['firstname']['str_eq'] == 'hej')]");
assertThat(compile("[?($['firstname']['str_eq'] == '')]").toString()).isEqualTo("[?($['firstname']['str_eq'] == '')]");
assertThat(compile("[?($['firstname']['str_eq'] == null)]").toString()).isEqualTo("[?($['firstname']['str_eq'] == null)]");
assertThat(compile("[?($['firstname']['str_eq'] == true)]").toString()).isEqualTo("[?($['firstname']['str_eq'] == true)]");
assertThat(compile("[?($['firstname']['str_eq'] == false)]").toString()).isEqualTo("[?($['firstname']['str_eq'] == false)]");
assertThat(compile("[?(@.firstname && @.lastname)]").toString()).isEqualTo("[?(@['firstname'] && @['lastname'])]");
assertThat(compile("[?((@.firstname || @.lastname) && @.and)]").toString()).isEqualTo("[?((@['firstname'] || @['lastname']) && @['and'])]");
assertThat(compile("[?((@.a || @.b || @.c) && @.x)]").toString()).isEqualTo("[?((@['a'] || @['b'] || @['c']) && @['x'])]");
assertThat(compile("[?((@.a && @.b && @.c) || @.x)]").toString()).isEqualTo("[?((@['a'] && @['b'] && @['c']) || @['x'])]");
assertThat(compile("[?((@.a && @.b || @.c) || @.x)]").toString()).isEqualTo("[?((@['a'] && (@['b'] || @['c'])) || @['x'])]");
assertThat(compile("[?((@.a && @.b) || (@.c && @.d))]").toString()).isEqualTo("[?((@['a'] && @['b']) || (@['c'] && @['d']))]");
assertThat(compile("[?(@.a IN [1,2,3])]").toString()).isEqualTo("[?(@['a'] IN [1,2,3])]");
assertThat(compile("[?(@.a IN {'foo':'bar'})]").toString()).isEqualTo("[?(@['a'] IN {'foo':'bar'})]");
assertThat(compile("[?(@.value<'7')]").toString()).isEqualTo("[?(@['value'] < '7')]");
assertThat(compile("[?(@.message == 'it\\\\')]").toString()).isEqualTo("[?(@['message'] == 'it\\\\')]");
}
}

17
json-path/src/test/java/com/jayway/jsonpath/internal/UtilsTest.java

@ -0,0 +1,17 @@
package com.jayway.jsonpath.internal;
import org.junit.Test;
public class UtilsTest {
@Test
public void strings_can_be_escaped() {
String str = "it\\\\";
System.out.println(Utils.unescape(str));
System.out.println(Utils.escape(str, true));
}
}
Loading…
Cancel
Save