com.jayway.jsonpath.internal.PathCompiler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of json-path Show documentation
Show all versions of json-path Show documentation
Java port of Stefan Goessner JsonPath.
/*
* Copyright 2011 the original author or authors.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.ArrayPathToken;
import com.jayway.jsonpath.internal.token.PathToken;
import com.jayway.jsonpath.internal.token.PredicatePathToken;
import com.jayway.jsonpath.internal.token.PropertyPathToken;
import com.jayway.jsonpath.internal.token.RootPathToken;
import com.jayway.jsonpath.internal.token.ScanPathToken;
import com.jayway.jsonpath.internal.token.WildcardPathToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import static com.jayway.jsonpath.internal.Utils.notEmpty;
import static java.util.Arrays.asList;
public class PathCompiler {
private static final Logger logger = LoggerFactory.getLogger(PathCompiler.class);
private static final String PROPERTY_OPEN = "['";
private static final String PROPERTY_CLOSE = "']";
private static final char DOCUMENT = '$';
private static final char ANY = '*';
private static final char PERIOD = '.';
private static final char BRACKET_OPEN = '[';
private static final char BRACKET_CLOSE = ']';
private static final char SPACE = ' ';
private static final Cache cache = new Cache(200);
public static Path compile(String path, Predicate... filters) {
notEmpty(path, "Path may not be null empty");
try {
path = path.trim();
if (path.endsWith("..")) {
throw new InvalidPathException("A path can not end with a scan.");
}
LinkedList filterList = new LinkedList(asList(filters));
if (path.charAt(0) != '$' && path.charAt(0) != '@') {
path = "$." + path;
}
boolean isRootPath = (path.charAt(0) == '$');
if (path.charAt(0) == '@') {
path = "$" + path.substring(1);
}
if (path.length() > 1 &&
path.charAt(1) != '.' &&
path.charAt(1) != '[') {
throw new InvalidPathException("Invalid path " + path);
}
String cacheKey = path + isRootPath + filterList.toString();
Path p = cache.get(cacheKey);
if (p != null) {
if (logger.isDebugEnabled()) logger.debug("Using cached path: " + cacheKey);
return p;
}
RootPathToken root = null;
int i = 0;
int positions;
String fragment = "";
do {
char current = path.charAt(i);
switch (current) {
case SPACE:
throw new InvalidPathException("Space not allowed in path");
case DOCUMENT:
fragment = "$";
i++;
break;
case BRACKET_OPEN:
positions = fastForwardUntilClosed(path, i);
fragment = path.substring(i, i + positions);
i += positions;
break;
case PERIOD:
i++;
if (path.charAt(i) == PERIOD) {
//This is a deep scan
fragment = "..";
i++;
} else {
positions = fastForward(path, i);
if (positions == 0) {
continue;
} else if (positions == 1 && path.charAt(i) == '*') {
fragment = new String("[*]");
} else {
assertValidFieldChars(path, i, positions);
fragment = PROPERTY_OPEN + path.substring(i, i + positions) + PROPERTY_CLOSE;
}
i += positions;
}
break;
case ANY:
fragment = new String("[*]");
i++;
break;
default:
positions = fastForward(path, i);
fragment = PROPERTY_OPEN + path.substring(i, i + positions) + PROPERTY_CLOSE;
i += positions;
break;
}
if (root == null) {
root = (RootPathToken) PathComponentAnalyzer.analyze(fragment, filterList);
} else {
root.append(PathComponentAnalyzer.analyze(fragment, filterList));
}
} while (i < path.length());
Path pa = new CompiledPath(root, isRootPath);
cache.put(cacheKey, pa);
return pa;
} catch (Exception ex){
throw new InvalidPathException(ex);
}
}
private static void assertValidFieldChars(String s, int start, int positions) {
/*
int i = start;
while (i < start + positions) {
char c = s.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '-' && c != '_' && c != '$' && c != '@') {
throw new InvalidPathException("Invalid field name! Use bracket notation if your filed names does not match pattern: ([a-zA-Z@][a-zA-Z0-9@\\$_\\-]*)$");
}
i++;
}
*/
}
private static int fastForward(String s, int index) {
int skipCount = 0;
while (index < s.length()) {
char current = s.charAt(index);
if (current == PERIOD || current == BRACKET_OPEN || current == SPACE) {
break;
}
index++;
skipCount++;
}
return skipCount;
}
private static int fastForwardUntilClosed(String s, int index) {
int skipCount = 0;
int nestedBrackets = 0;
//First char is always '[' no need to check it
index++;
skipCount++;
while (index < s.length()) {
char current = s.charAt(index);
index++;
skipCount++;
if (current == BRACKET_CLOSE && nestedBrackets == 0) {
break;
}
if (current == BRACKET_OPEN) {
nestedBrackets++;
}
if (current == BRACKET_CLOSE) {
nestedBrackets--;
}
}
return skipCount;
}
//---------------------------------------------
//
//
//
//---------------------------------------------
static class PathComponentAnalyzer {
private static final Pattern FILTER_PATTERN = Pattern.compile("^\\[\\s*\\?\\s*[,\\s*\\?]*?\\s*]$"); //[?] or [?, ?, ...]
private int i;
private char current;
private final LinkedList filterList;
private final String pathFragment;
PathComponentAnalyzer(String pathFragment, LinkedList filterList) {
this.pathFragment = pathFragment;
this.filterList = filterList;
}
static PathToken analyze(String pathFragment, LinkedList filterList) {
return new PathComponentAnalyzer(pathFragment, filterList).analyze();
}
public PathToken analyze() {
if ("$".equals(pathFragment)) return new RootPathToken();
else if ("..".equals(pathFragment)) return new ScanPathToken();
else if ("[*]".equals(pathFragment)) return new WildcardPathToken();
else if (".*".equals(pathFragment)) return new WildcardPathToken();
else if ("[?]".equals(pathFragment)) return new PredicatePathToken(filterList.poll());
else if (FILTER_PATTERN.matcher(pathFragment).matches()) {
final int criteriaCount = Utils.countMatches(pathFragment, "?");
List filters = new ArrayList(criteriaCount);
for (int i = 0; i < criteriaCount; i++) {
filters.add(filterList.poll());
}
return new PredicatePathToken(filters);
}
this.i = 0;
do {
current = pathFragment.charAt(i);
switch (current) {
case '?':
return analyzeCriteriaSequence4();
case '\'':
return analyzeProperty();
default:
if (Character.isDigit(current) || current == ':' || current == '-' || current == '@') {
return analyzeArraySequence();
}
i++;
break;
}
} while (i < pathFragment.length());
throw new InvalidPathException("Could not analyze path component: " + pathFragment);
}
public PathToken analyzeCriteriaSequence4() {
int[] bounds = findFilterBounds();
i = bounds[1];
return new PredicatePathToken(Filter.parse(pathFragment.substring(bounds[0], bounds[1])));
}
int[] findFilterBounds(){
int end = 0;
int start = i;
while(pathFragment.charAt(start) != '['){
start--;
}
int mem = ' ';
int curr = start;
boolean inProp = false;
int openSquareBracket = 0;
int openBrackets = 0;
while(end == 0){
char c = pathFragment.charAt(curr);
switch (c){
case '(':
if(!inProp) openBrackets++;
break;
case ')':
if(!inProp) openBrackets--;
break;
case '[':
if(!inProp) openSquareBracket++;
break;
case ']':
if(!inProp){
openSquareBracket--;
if(openBrackets == 0){
end = curr + 1;
}
}
break;
case '\'':
if(mem == '\\') {
break;
}
inProp = !inProp;
break;
default:
break;
}
mem = c;
curr++;
}
if(openBrackets != 0 || openSquareBracket != 0){
throw new InvalidPathException("Filter brackets are not balanced");
}
return new int[]{start, end};
}
//"['foo']"
private PathToken analyzeProperty() {
List properties = new ArrayList();
StringBuilder buffer = new StringBuilder();
boolean propertyIsOpen = false;
while (current != ']') {
switch (current) {
case '\'':
if (propertyIsOpen) {
properties.add(buffer.toString());
buffer.setLength(0);
propertyIsOpen = false;
} else {
propertyIsOpen = true;
}
break;
default:
if (propertyIsOpen) {
buffer.append(current);
}
break;
}
current = pathFragment.charAt(++i);
}
return new PropertyPathToken(properties);
}
//"[-1:]" sliceFrom
//"[:1]" sliceTo
//"[0:5]" sliceBetween
//"[1]"
//"[1,2,3]"
//"[(@.length - 1)]"
private PathToken analyzeArraySequence() {
StringBuilder buffer = new StringBuilder();
List numbers = new ArrayList();
boolean contextSize = (current == '@');
boolean sliceTo = false;
boolean sliceFrom = false;
boolean sliceBetween = false;
boolean indexSequence = false;
boolean singleIndex = false;
if (contextSize) {
current = pathFragment.charAt(++i);
current = pathFragment.charAt(++i);
while (current != '-') {
if (current == ' ' || current == '(' || current == ')') {
current = pathFragment.charAt(++i);
continue;
}
buffer.append(current);
current = pathFragment.charAt(++i);
}
String function = buffer.toString();
buffer.setLength(0);
if (!function.equals("size") && !function.equals("length")) {
throw new InvalidPathException("Invalid function: @." + function + ". Supported functions are: [(@.length - n)] and [(@.size() - n)]");
}
while (current != ')') {
if (current == ' ') {
current = pathFragment.charAt(++i);
continue;
}
buffer.append(current);
current = pathFragment.charAt(++i);
}
} else {
while (Character.isDigit(current) || current == ',' || current == ' ' || current == ':' || current == '-') {
switch (current) {
case ' ':
break;
case ':':
if (buffer.length() == 0) {
//this is a tail slice [:12]
sliceTo = true;
current = pathFragment.charAt(++i);
while (Character.isDigit(current) || current == ' ' || current == '-') {
if (current != ' ') {
buffer.append(current);
}
current = pathFragment.charAt(++i);
}
numbers.add(Integer.parseInt(buffer.toString()));
buffer.setLength(0);
} else {
//we now this starts with [12:???
numbers.add(Integer.parseInt(buffer.toString()));
buffer.setLength(0);
current = pathFragment.charAt(++i);
//this is a tail slice [:12]
while (Character.isDigit(current) || current == ' ' || current == '-') {
if (current != ' ') {
buffer.append(current);
}
current = pathFragment.charAt(++i);
}
if (buffer.length() == 0) {
sliceFrom = true;
} else {
sliceBetween = true;
numbers.add(Integer.parseInt(buffer.toString()));
buffer.setLength(0);
}
}
break;
case ',':
numbers.add(Integer.parseInt(buffer.toString()));
buffer.setLength(0);
indexSequence = true;
break;
default:
buffer.append(current);
break;
}
if (current == ']') {
break;
}
current = pathFragment.charAt(++i);
}
}
if (buffer.length() > 0) {
numbers.add(Integer.parseInt(buffer.toString()));
}
singleIndex = (numbers.size() == 1) && !sliceTo && !sliceFrom && !contextSize;
if (logger.isTraceEnabled()) {
logger.debug("numbers are : {}", numbers.toString());
logger.debug("sequence is singleNumber : {}", singleIndex);
logger.debug("sequence is numberSequence : {}", indexSequence);
logger.debug("sequence is sliceFrom : {}", sliceFrom);
logger.debug("sequence is sliceTo : {}", sliceTo);
logger.debug("sequence is sliceBetween : {}", sliceBetween);
logger.debug("sequence is contextFetch : {}", contextSize);
logger.debug("---------------------------------------------");
}
ArrayPathToken.Operation operation = null;
if (singleIndex) operation = ArrayPathToken.Operation.SINGLE_INDEX;
else if (indexSequence) operation = ArrayPathToken.Operation.INDEX_SEQUENCE;
else if (sliceFrom) operation = ArrayPathToken.Operation.SLICE_FROM;
else if (sliceTo) operation = ArrayPathToken.Operation.SLICE_TO;
else if (sliceBetween) operation = ArrayPathToken.Operation.SLICE_BETWEEN;
else if (contextSize) operation = ArrayPathToken.Operation.CONTEXT_SIZE;
assert operation != null;
return new ArrayPathToken(numbers, operation);
}
}
}