blob: fcfb972a1f09d536f94641950b931b32f241644d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.knox.gateway.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
public class JsonPath {
public static class Match {
private Match parent;
private Segment segment;
private JsonNode node;
private String field;
private int index;
private Match( Match parent, Segment segment, JsonNode node, String field, int index ) {
this.parent = parent;
this.segment = segment;
this.node = node;
this.field = field;
this.index = index;
}
private Match( Match parent, Segment segment, JsonNode node, String field ) {
this( parent, segment, node, field, -1 );
}
private Match( Match parent, Segment segment, JsonNode node, int index ) {
this( parent, segment, node, null, index );
}
public Match getParent() {
return parent;
}
public Segment getSegment() {
return segment;
}
public JsonNode getNode() {
return node;
}
public String getField() {
return field;
}
public int getIndex() {
return index;
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append( "JsonPath{" );
s.append( "parent=" ); s.append( getParent() );
s.append( ",segment=" ); s.append( getSegment() );
s.append( ",node=" ); s.append( getNode() );
s.append( ",field=" ); s.append( getField() );
s.append( ",index=" ); s.append( getIndex() );
s.append( "}" );
return s.toString();
}
}
public static class Expression {
private List<Segment> segments;
private Expression( String expression ) {
if( expression == null || !expression.startsWith( "$" ) ) {
throw new IllegalArgumentException( expression );
}
segments = parse( expression );
}
public Segment[] getSegments() {
Segment[] array = null;
if( segments != null ) {
array = new Segment[ segments.size() ];
segments.toArray( array );
}
return array;
}
private List<Segment> parse( String expression ) {
boolean insideBrackets = false;
boolean expectChild = false;
boolean foundChild = false;
List<Segment> list = null;
Segment segment;
StringTokenizer parser = new StringTokenizer( expression, "$.[]()@?:,", true );
String currToken = null;
String prevToken = null;
while( parser.hasMoreTokens() ) {
prevToken = currToken;
if( insideBrackets ) {
currToken = parser.nextToken( "$[]()@?:," ).trim();
} else {
currToken = parser.nextToken( "$.[]()@?:," ).trim();
}
char c = currToken.charAt( 0 );
switch( c ) {
case '$' :
if( list != null ) {
throw new IllegalArgumentException( expression );
}
list = new ArrayList<>();
segment = new Segment( Segment.Type.ROOT );
list.add( segment );
break;
case '.' :
if( expectChild ) {
if( ".".equals( prevToken ) ) {
segment = new Segment( Segment.Type.GLOB );
list.add( segment );
expectChild = true;
foundChild = false;
} else {
throw new IllegalArgumentException( expression );
}
} else {
expectChild = true;
foundChild = false;
}
break;
case '[' :
if( expectChild ) {
throw new IllegalArgumentException( expression );
}
insideBrackets = true;
expectChild = true;
foundChild = false;
break;
case ']' :
if( !foundChild ) {
throw new IllegalArgumentException( expression );
}
insideBrackets = false;
expectChild = false;
foundChild = false;
break;
case '(':
case ')':
case '@':
case '?':
case ':':
case ',':
throw new IllegalArgumentException( expression );
default:
if( !expectChild ) {
throw new IllegalArgumentException( expression );
} else if( Character.isDigit( c ) ) {
try {
segment = new Segment( Integer.parseInt( currToken ) );
} catch( NumberFormatException e ) {
throw new IllegalArgumentException( expression, e );
}
} else {
if( "*".equals( currToken ) ) {
segment = new Segment( Segment.Type.WILD );
} else if ( "**".equals( currToken ) ) {
segment = new Segment( Segment.Type.GLOB );
} else {
if( currToken.startsWith( "'" ) ) {
currToken = currToken.substring( 1 );
}
if( currToken.endsWith( "'" ) ) {
currToken = currToken.substring( 0, currToken.length()-1 );
}
segment = new Segment( currToken );
}
}
list.add( segment );
expectChild = false;
foundChild = true;
break;
}
}
if( expectChild && !foundChild ) {
throw new IllegalArgumentException( expression );
}
return list;
}
public List<Match> evaluate( JsonNode root ) {
JsonNode parent;
JsonNode child;
Match newMatch;
List<Match> tempMatches;
List<Match> oldMatches = new ArrayList<>();
List<Match> newMatches = new ArrayList<>();
if( root != null ) {
for( Segment seg : segments ) {
if( Segment.Type.ROOT == seg.getType() ) {
oldMatches.add( new Match( null, segments.get( 0 ), root, null, -1 ) );
continue;
} else {
for( Match oldMatch : oldMatches ) {
parent = oldMatch.getNode();
switch( seg.getType() ) {
case FIELD:
if( JsonNodeType.OBJECT == oldMatch.getNode().getNodeType() ) {
child = oldMatch.getNode().get( seg.getField() );
if( child == null ) {
continue;
} else {
newMatches.add( new Match( oldMatch, seg, child, seg.getField() ) );
}
} else {
continue;
}
break;
case INDEX:
if( JsonNodeType.ARRAY == oldMatch.getNode().getNodeType() ) {
child = oldMatch.getNode().get( seg.getIndex() );
if( child == null ) {
continue;
} else {
newMatches.add( new Match( oldMatch, seg, child, seg.getIndex() ) );
}
} else {
continue;
}
break;
case GLOB:
newMatches.add( oldMatch );
case WILD:
switch( parent.getNodeType() ) {
case OBJECT:
Iterator<Map.Entry<String,JsonNode>> fields = parent.fields();
while( fields.hasNext() ) {
Map.Entry<String,JsonNode> field = fields.next();
newMatch = new Match( oldMatch, seg, field.getValue(), field.getKey() );
newMatches.add( newMatch );
if( seg.getType() == Segment.Type.GLOB ) {
addAllChildren( oldMatch, newMatches, field.getValue() );
}
}
break;
case ARRAY:
for( int i=0, n=parent.size(); i<n; i++ ) {
newMatch = new Match( oldMatch, seg, parent.get( i ), i );
newMatches.add( newMatch );
if( seg.getType() == Segment.Type.GLOB ) {
addAllChildren( oldMatch, newMatches, newMatch.getNode() );
}
}
break;
}
break;
default:
throw new IllegalStateException();
}
}
if( newMatches.isEmpty() ) {
return newMatches;
} else {
tempMatches = oldMatches;
oldMatches = newMatches;
newMatches = tempMatches;
newMatches.clear();
}
}
}
}
return oldMatches;
}
private static final void addAllChildren( Match parent, List<Match> matches, JsonNode node ) {
Match match;
switch( node.getNodeType() ) {
case OBJECT:
Iterator<Map.Entry<String,JsonNode>> fields = node.fields();
while( fields.hasNext() ) {
Map.Entry<String,JsonNode> field = fields.next();
match = new Match( parent, parent.getSegment(), field.getValue(), field.getKey() );
matches.add( match );
addAllChildren( match, matches, match.getNode() );
}
break;
case ARRAY:
for( int i=0, n=node.size(); i<n; i++ ) {
match = new Match( parent, parent.getSegment(), node.get( i ), i );
matches.add( match );
addAllChildren( match, matches, match.getNode() );
}
break;
}
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append( "JsonPath.Expression{" );
for( int i=0, n=segments.size(); i<n; i++ ) {
if( i > 0 ) {
s.append( "," );
}
s.append( "segment[" );
s.append( i );
s.append( "]=" );
s.append( segments.get( i ) );
}
s.append( "]" );
return s.toString();
}
}
public static class Segment {
public enum Type { ROOT, FIELD, INDEX, WILD, GLOB }
private Type type;
private String field;
private int index;
private Segment( Type type ) {
this.type = type;
this.field = null;
this.index = -1;
}
private Segment( String field ) {
this.type = Type.FIELD;
this.field = field;
this.index = -1;
}
private Segment( int index ) {
this.type = Type.INDEX;
this.field = null;
this.index = index;
}
public Type getType() {
return type;
}
public String getField() {
return field;
}
public int getIndex() {
return index;
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append( "JsonPath.Segment{" );
s.append( "type=" ); s.append( getType() );
s.append( ",field=" ); s.append( getField() );
s.append( ",index=" ); s.append( getIndex() );
s.append( "}" );
return s.toString();
}
}
public static Expression compile( String expression ) {
return new Expression( expression );
}
}