You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
589 lines
20 KiB
589 lines
20 KiB
package com.jayway.jsonpath.internal; |
|
|
|
import com.jayway.jsonpath.Filter; |
|
import com.jayway.jsonpath.InvalidPathException; |
|
import com.jayway.jsonpath.Predicate; |
|
import com.jayway.jsonpath.internal.token.ArrayIndexOperation; |
|
import com.jayway.jsonpath.internal.token.ArraySliceOperation; |
|
import com.jayway.jsonpath.internal.token.FunctionPathToken; |
|
import com.jayway.jsonpath.internal.token.PathTokenAppender; |
|
import com.jayway.jsonpath.internal.token.PathTokenFactory; |
|
import com.jayway.jsonpath.internal.token.RootPathToken; |
|
|
|
import java.util.ArrayList; |
|
import java.util.Collection; |
|
import java.util.LinkedList; |
|
import java.util.List; |
|
|
|
import static java.lang.Character.isDigit; |
|
import static java.lang.Math.min; |
|
import static java.util.Arrays.asList; |
|
|
|
public class PathCompiler { |
|
|
|
private static final char DOC_CONTEXT = '$'; |
|
private static final char EVAL_CONTEXT = '@'; |
|
private static final char OPEN_SQUARE_BRACKET = '['; |
|
private static final char CLOSE_SQUARE_BRACKET = ']'; |
|
private static final char OPEN_BRACKET = '('; |
|
private static final char CLOSE_BRACKET = ')'; |
|
private static final char WILDCARD = '*'; |
|
private static final char PERIOD = '.'; |
|
private static final char SPACE = ' '; |
|
private static final char QUESTIONMARK = '?'; |
|
private static final char COMMA = ','; |
|
private static final char SPLIT = ':'; |
|
private static final char MINUS = '-'; |
|
private static final char ESCAPE = '\\'; |
|
private static final char TICK = '\''; |
|
private static final char FUNCTION = '%'; |
|
|
|
private static final Cache cache = new Cache(200); |
|
|
|
private final LinkedList<Predicate> filterStack; |
|
private final CharacterIndex path; |
|
|
|
private PathCompiler(String path, LinkedList<Predicate> filterStack) { |
|
this.filterStack = filterStack; |
|
this.path = new CharacterIndex(path); |
|
} |
|
|
|
private Path compile() { |
|
RootPathToken root = readContextToken(); |
|
return new CompiledPath(root, root.getPathFragment().equals("$")); |
|
} |
|
|
|
public static Path compile(String path, final Predicate... filters) { |
|
try { |
|
path = path.trim(); |
|
|
|
if(!path.startsWith("$") && !path.startsWith("@")){ |
|
path = "$." + path; |
|
} |
|
if(path.endsWith("..")){ |
|
fail("Path must not end wid a scan operation '..'"); |
|
} |
|
LinkedList filterStack = new LinkedList<Predicate>(asList(filters)); |
|
String cacheKey = Utils.concat(path, filterStack.toString()); |
|
Path p = cache.get(cacheKey); |
|
if (p == null) { |
|
p = new PathCompiler(path.trim(), filterStack).compile(); |
|
cache.put(cacheKey, p); |
|
} |
|
return p; |
|
} catch (Exception e) { |
|
InvalidPathException ipe; |
|
if (e instanceof InvalidPathException) { |
|
ipe = (InvalidPathException) e; |
|
} else { |
|
ipe = new InvalidPathException(e); |
|
} |
|
throw ipe; |
|
} |
|
} |
|
|
|
//[$ | @] |
|
private RootPathToken readContextToken() { |
|
|
|
if (!path.currentCharIs(DOC_CONTEXT) && !path.currentCharIs(EVAL_CONTEXT)) { |
|
throw new InvalidPathException("Path must start with '$' or '@'"); |
|
} |
|
|
|
RootPathToken pathToken = PathTokenFactory.createRootPathToken(path.currentChar()); |
|
PathTokenAppender appender = pathToken.getPathTokenAppender(); |
|
|
|
if (path.currentIsTail()) { |
|
return pathToken; |
|
} |
|
|
|
path.incrementPosition(1); |
|
|
|
if(path.currentChar() != PERIOD && path.currentChar() != OPEN_SQUARE_BRACKET){ |
|
fail("Illegal character at position " + path.position + " expected '.' or '["); |
|
} |
|
|
|
readNextToken(appender); |
|
|
|
return pathToken; |
|
} |
|
|
|
// |
|
// |
|
// |
|
private boolean readNextToken(PathTokenAppender appender) { |
|
|
|
char c = path.currentChar(); |
|
|
|
switch (c) { |
|
case OPEN_SQUARE_BRACKET: |
|
return readBracketPropertyToken(appender) || |
|
readArrayToken(appender) || |
|
readWildCardToken(appender) || |
|
readFilterToken(appender) || |
|
readPlaceholderToken(appender) || |
|
fail("Could not parse bracket statement at position " + path.position); |
|
case PERIOD: |
|
return readDotSeparatorToken(appender) || |
|
readScanToken(appender) || |
|
fail("Could not parse token at position " + path.position); |
|
case WILDCARD: |
|
return readWildCardToken(appender) || |
|
fail("Could not parse token at position " + path.position); |
|
case FUNCTION: |
|
return readFunctionToken(appender) || |
|
fail("Could not parse token at position " + path.position); |
|
default: |
|
return readPropertyToken(appender) || |
|
fail("Could not parse token at position " + path.position); |
|
} |
|
} |
|
|
|
// |
|
// $function() |
|
// |
|
private boolean readFunctionToken(PathTokenAppender appender) { |
|
if (path.currentCharIs(OPEN_SQUARE_BRACKET) || path.currentCharIs(WILDCARD) || path.currentCharIs(PERIOD) || path.currentCharIs(SPACE)) { |
|
return false; |
|
} |
|
int startPosition = path.position; |
|
int readPosition = startPosition; |
|
int endPosition = 0; |
|
while (path.inBounds(readPosition)) { |
|
char c = path.charAt(readPosition); |
|
if (c == OPEN_BRACKET && path.nextSignificantCharIs(readPosition, CLOSE_BRACKET)) { |
|
endPosition = path.indexOfNextSignificantChar(readPosition, CLOSE_BRACKET); |
|
break; |
|
} |
|
readPosition++; |
|
} |
|
path.setPosition(endPosition); |
|
|
|
String function = path.subSequence(startPosition, endPosition + 1).toString(); |
|
|
|
appender.appendPathToken(PathTokenFactory.createFunctionPathToken(function)); |
|
|
|
return path.currentIsTail(); |
|
} |
|
|
|
// |
|
// . |
|
// |
|
private boolean readDotSeparatorToken(PathTokenAppender appender) { |
|
if (!path.currentCharIs('.') || path.nextCharIs('.')) { |
|
return false; |
|
} |
|
if (!path.hasMoreCharacters()) { |
|
throw new InvalidPathException("Path must not end with a '."); |
|
} |
|
// if (path.nextSignificantCharIs('[')) { |
|
// throw new InvalidPathException("A bracket may not follow a '."); |
|
// } |
|
|
|
path.incrementPosition(1); |
|
|
|
return readNextToken(appender); |
|
} |
|
|
|
// |
|
// fooBar |
|
// |
|
private boolean readPropertyToken(PathTokenAppender appender) { |
|
if (path.currentCharIs(OPEN_SQUARE_BRACKET) || path.currentCharIs(WILDCARD) || path.currentCharIs(PERIOD) || path.currentCharIs(SPACE)) { |
|
return false; |
|
} |
|
int startPosition = path.position; |
|
int readPosition = startPosition; |
|
int endPosition = 0; |
|
|
|
while (path.inBounds(readPosition)) { |
|
char c = path.charAt(readPosition); |
|
if (c == SPACE) { |
|
throw new InvalidPathException("Use bracket notion ['my prop'] if your property contains blank characters. position: " + path.position); |
|
} |
|
if (c == PERIOD || c == OPEN_SQUARE_BRACKET) { |
|
endPosition = readPosition; |
|
break; |
|
} |
|
readPosition++; |
|
} |
|
if (endPosition == 0) { |
|
endPosition = path.length(); |
|
} |
|
|
|
path.setPosition(endPosition); |
|
|
|
String property = path.subSequence(startPosition, endPosition).toString(); |
|
|
|
appender.appendPathToken(PathTokenFactory.createSinglePropertyPathToken(property)); |
|
|
|
return path.currentIsTail() || readNextToken(appender); |
|
} |
|
|
|
// |
|
// [?], [?,?, ..] |
|
// |
|
private boolean readPlaceholderToken(PathTokenAppender appender) { |
|
|
|
if (!path.currentCharIs(OPEN_SQUARE_BRACKET)) { |
|
return false; |
|
} |
|
int questionmarkIndex = path.indexOfNextSignificantChar(QUESTIONMARK); |
|
if (questionmarkIndex == -1) { |
|
return false; |
|
} |
|
char nextSignificantChar = path.nextSignificantChar(questionmarkIndex); |
|
if (nextSignificantChar != CLOSE_SQUARE_BRACKET && nextSignificantChar != COMMA) { |
|
return false; |
|
} |
|
|
|
int expressionBeginIndex = path.position + 1; |
|
int expressionEndIndex = path.nextIndexOf(expressionBeginIndex, CLOSE_SQUARE_BRACKET); |
|
|
|
if (expressionEndIndex == -1) { |
|
return false; |
|
} |
|
|
|
String expression = path.subSequence(expressionBeginIndex, expressionEndIndex).toString(); |
|
|
|
String[] tokens = expression.split(","); |
|
|
|
if (filterStack.size() < tokens.length) { |
|
throw new InvalidPathException("Not enough predicates supplied for filter [" + expression + "] at position " + path.position); |
|
} |
|
|
|
Collection<Predicate> predicates = new ArrayList<Predicate>(); |
|
for (String token : tokens) { |
|
token = token != null ? token.trim() : token; |
|
if (!"?".equals(token == null ? "" : token)) { |
|
throw new InvalidPathException("Expected '?' but found " + token); |
|
} |
|
predicates.add(filterStack.pop()); |
|
} |
|
|
|
appender.appendPathToken(PathTokenFactory.createPredicatePathToken(predicates)); |
|
|
|
path.setPosition(expressionEndIndex + 1); |
|
|
|
return path.currentIsTail() || readNextToken(appender); |
|
} |
|
|
|
// |
|
// [?(...)] |
|
// |
|
private boolean readFilterToken(PathTokenAppender appender) { |
|
if (!path.currentCharIs(OPEN_SQUARE_BRACKET) && !path.nextSignificantCharIs(QUESTIONMARK)) { |
|
return false; |
|
} |
|
|
|
int openStatementBracketIndex = path.position; |
|
int questionMarkIndex = path.indexOfNextSignificantChar(QUESTIONMARK); |
|
if (questionMarkIndex == -1) { |
|
return false; |
|
} |
|
int openBracketIndex = path.indexOfNextSignificantChar(questionMarkIndex, OPEN_BRACKET); |
|
if (openBracketIndex == -1) { |
|
return false; |
|
} |
|
int closeBracketIndex = path.indexOfClosingBracket(openBracketIndex + 1, true); |
|
if (closeBracketIndex == -1) { |
|
return false; |
|
} |
|
if (!path.nextSignificantCharIs(closeBracketIndex, CLOSE_SQUARE_BRACKET)) { |
|
return false; |
|
} |
|
int closeStatementBracketIndex = path.indexOfNextSignificantChar(closeBracketIndex, CLOSE_SQUARE_BRACKET); |
|
|
|
String criteria = path.subSequence(openStatementBracketIndex, closeStatementBracketIndex + 1).toString(); |
|
|
|
appender.appendPathToken(PathTokenFactory.createPredicatePathToken(Filter.parse(criteria))); |
|
|
|
path.setPosition(closeStatementBracketIndex + 1); |
|
|
|
return path.currentIsTail() || readNextToken(appender); |
|
|
|
} |
|
|
|
// |
|
// [*] |
|
// * |
|
// |
|
private boolean readWildCardToken(PathTokenAppender appender) { |
|
|
|
boolean inBracket = path.currentCharIs(OPEN_SQUARE_BRACKET); |
|
|
|
if (inBracket && !path.nextSignificantCharIs(WILDCARD)) { |
|
return false; |
|
} |
|
if (!path.currentCharIs(WILDCARD) && path.isOutOfBounds(path.position + 1)) { |
|
return false; |
|
} |
|
if (inBracket) { |
|
int wildCardIndex = path.indexOfNextSignificantChar(WILDCARD); |
|
if (!path.nextSignificantCharIs(wildCardIndex, CLOSE_SQUARE_BRACKET)) { |
|
throw new InvalidPathException("Expected wildcard token to end with ']' on position " + wildCardIndex + 1); |
|
} |
|
int bracketCloseIndex = path.indexOfNextSignificantChar(wildCardIndex, CLOSE_SQUARE_BRACKET); |
|
path.setPosition(bracketCloseIndex + 1); |
|
} else { |
|
path.incrementPosition(1); |
|
} |
|
|
|
appender.appendPathToken(PathTokenFactory.createWildCardPathToken()); |
|
|
|
return path.currentIsTail() || readNextToken(appender); |
|
} |
|
|
|
// |
|
// [1], [1,2, n], [1:], [1:2], [:2] |
|
// |
|
private boolean readArrayToken(PathTokenAppender appender) { |
|
|
|
if (!path.currentCharIs(OPEN_SQUARE_BRACKET)) { |
|
return false; |
|
} |
|
char nextSignificantChar = path.nextSignificantChar(); |
|
if (!isDigit(nextSignificantChar) && nextSignificantChar != MINUS && nextSignificantChar != SPLIT) { |
|
return false; |
|
} |
|
|
|
int expressionBeginIndex = path.position + 1; |
|
int expressionEndIndex = path.nextIndexOf(expressionBeginIndex, CLOSE_SQUARE_BRACKET); |
|
|
|
if (expressionEndIndex == -1) { |
|
return false; |
|
} |
|
|
|
String expression = path.subSequence(expressionBeginIndex, expressionEndIndex).toString().replace(" ", ""); |
|
|
|
if ("*".equals(expression)) { |
|
return false; |
|
} |
|
|
|
//check valid chars |
|
for (int i = 0; i < expression.length(); i++) { |
|
char c = expression.charAt(i); |
|
if (!isDigit(c) && c != COMMA && c != MINUS && c != SPLIT) { |
|
return false; |
|
} |
|
} |
|
|
|
boolean isSliceOperation = expression.contains(":"); |
|
|
|
if (isSliceOperation) { |
|
ArraySliceOperation arraySliceOperation = ArraySliceOperation.parse(expression); |
|
appender.appendPathToken(PathTokenFactory.createSliceArrayPathToken(arraySliceOperation)); |
|
} else { |
|
ArrayIndexOperation arrayIndexOperation = ArrayIndexOperation.parse(expression); |
|
appender.appendPathToken(PathTokenFactory.createIndexArrayPathToken(arrayIndexOperation)); |
|
} |
|
|
|
path.setPosition(expressionEndIndex + 1); |
|
|
|
return path.currentIsTail() || readNextToken(appender); |
|
} |
|
|
|
// |
|
// ['foo'] |
|
// |
|
private boolean readBracketPropertyToken(PathTokenAppender appender) { |
|
if (!path.currentCharIs(OPEN_SQUARE_BRACKET) || !path.nextSignificantCharIs(TICK)) { |
|
return false; |
|
} |
|
|
|
List<String> properties = new ArrayList<String>(); |
|
|
|
int startPosition = path.position + 1; |
|
int readPosition = startPosition; |
|
int endPosition = 0; |
|
boolean inProperty = false; |
|
|
|
while (path.inBounds(readPosition)) { |
|
char c = path.charAt(readPosition); |
|
|
|
if (c == CLOSE_SQUARE_BRACKET && !inProperty) { |
|
break; |
|
} else if (c == TICK) { |
|
if (inProperty) { |
|
endPosition = readPosition; |
|
properties.add(path.subSequence(startPosition, endPosition).toString()); |
|
inProperty = false; |
|
} else { |
|
startPosition = readPosition + 1; |
|
inProperty = true; |
|
} |
|
} |
|
readPosition++; |
|
} |
|
|
|
int endBracketIndex = path.indexOfNextSignificantChar(endPosition, CLOSE_SQUARE_BRACKET) + 1; |
|
|
|
path.setPosition(endBracketIndex); |
|
|
|
appender.appendPathToken(PathTokenFactory.createPropertyPathToken(properties)); |
|
|
|
return path.currentIsTail() || readNextToken(appender); |
|
} |
|
|
|
// |
|
// .. |
|
// |
|
private boolean readScanToken(PathTokenAppender appender) { |
|
if (!path.currentCharIs(PERIOD) || !path.nextCharIs(PERIOD)) { |
|
return false; |
|
} |
|
appender.appendPathToken(PathTokenFactory.crateScanToken()); |
|
path.incrementPosition(2); |
|
|
|
return readNextToken(appender); |
|
} |
|
|
|
|
|
public static boolean fail(String message) { |
|
throw new InvalidPathException(message); |
|
} |
|
|
|
|
|
private static class CharacterIndex { |
|
|
|
private final CharSequence charSequence; |
|
private int position; |
|
|
|
private CharacterIndex(CharSequence charSequence) { |
|
this.charSequence = charSequence; |
|
this.position = 0; |
|
} |
|
|
|
private int length() { |
|
return charSequence.length(); |
|
} |
|
|
|
private char charAt(int idx) { |
|
return charSequence.charAt(idx); |
|
} |
|
|
|
private char currentChar() { |
|
return charSequence.charAt(position); |
|
} |
|
|
|
private boolean currentCharIs(char c) { |
|
return (charSequence.charAt(position) == c); |
|
} |
|
|
|
private boolean nextCharIs(char c) { |
|
return inBounds(position + 1) && (charSequence.charAt(position + 1) == c); |
|
} |
|
|
|
private int incrementPosition(int charCount) { |
|
return setPosition(position + charCount); |
|
} |
|
|
|
private int setPosition(int newPosition) { |
|
position = min(newPosition, charSequence.length() - 1); |
|
return position; |
|
} |
|
|
|
private int indexOfClosingBracket(int startPosition, boolean skipStrings) { |
|
int opened = 1; |
|
int readPosition = startPosition; |
|
while (inBounds(readPosition)) { |
|
if (skipStrings) { |
|
if (charAt(readPosition) == TICK) { |
|
while (inBounds(readPosition)) { |
|
readPosition++; |
|
if (charAt(readPosition) == TICK && charAt(readPosition - 1) != ESCAPE) { |
|
readPosition++; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
if (charAt(readPosition) == OPEN_BRACKET) { |
|
opened++; |
|
} |
|
if (charAt(readPosition) == CLOSE_BRACKET) { |
|
opened--; |
|
if(opened == 0){ |
|
return readPosition; |
|
} |
|
} |
|
readPosition++; |
|
} |
|
return -1; |
|
} |
|
|
|
public int indexOfNextSignificantChar(char c) { |
|
return indexOfNextSignificantChar(position, c); |
|
} |
|
|
|
public int indexOfNextSignificantChar(int startPosition, char c) { |
|
int readPosition = startPosition + 1; |
|
while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) { |
|
readPosition++; |
|
} |
|
if (charAt(readPosition) == c) { |
|
return readPosition; |
|
} else { |
|
return -1; |
|
} |
|
} |
|
|
|
public int nextIndexOf(int startPosition, char c) { |
|
int readPosition = startPosition; |
|
while (!isOutOfBounds(readPosition)) { |
|
if (charAt(readPosition) == c) { |
|
return readPosition; |
|
} |
|
readPosition++; |
|
} |
|
return -1; |
|
} |
|
|
|
public boolean nextSignificantCharIs(int startPosition, char c) { |
|
int readPosition = startPosition + 1; |
|
while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) { |
|
readPosition++; |
|
} |
|
return !isOutOfBounds(readPosition) && charAt(readPosition) == c; |
|
} |
|
|
|
public boolean nextSignificantCharIs(char c) { |
|
return nextSignificantCharIs(position, c); |
|
} |
|
|
|
public char nextSignificantChar() { |
|
return nextSignificantChar(position); |
|
} |
|
|
|
public char nextSignificantChar(int startPosition) { |
|
int readPosition = startPosition + 1; |
|
while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) { |
|
readPosition++; |
|
} |
|
if (!isOutOfBounds(readPosition)) { |
|
return charAt(readPosition); |
|
} else { |
|
return ' '; |
|
} |
|
} |
|
|
|
private boolean currentIsTail() { |
|
return isOutOfBounds(position + 1); |
|
} |
|
|
|
private boolean hasMoreCharacters() { |
|
return inBounds(position + 1); |
|
} |
|
|
|
private boolean inBounds(int idx) { |
|
return (idx >= 0) && (idx < charSequence.length()); |
|
} |
|
|
|
private boolean isOutOfBounds(int idx) { |
|
return !inBounds(idx); |
|
} |
|
|
|
private CharSequence subSequence(int start, int end) { |
|
return charSequence.subSequence(start, end); |
|
} |
|
} |
|
}
|
|
|