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 filterStack; private final CharacterIndex path; private PathCompiler(String path, LinkedList 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(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 predicates = new ArrayList(); 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 properties = new ArrayList(); 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); } } }