blob: d307156194239cad6b3d2ad2bb9e7329be4cb436 [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.urltemplate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/*
Path
Match
{path} => {path=*}
{path=*} // Match single path level. (ie wildcard)
{path=**} // Match multiple path levels. (ie glob)
{path=*.ext} // Match single level with simplified regex pattern.
Expand
{path} => {path=**} // Note: Default cardinality changes between match and expand.
Query
Match
{queryParam} => {queryParam=*:queryParam}
{queryParam=*} => {queryParam=*:queryParam} // Match single queryParam value.
{queryParam=**} => {queryParam=**:queryParam} // Match multiple queryParam values.
{queryParam=*suffix:other-queryParam}
Expand
{queryParam} -> {queryParam=**:queryParam} // Note: Default cardinality changes between match and expand.
{queryParam=*} -> {queryParam=*:queryParam}
{queryParam=**} -> {queryParam=**:queryParam}
{queryParam=other-parm} -> {queryParam=**:otherparam} // Note: Default cardinality changes between match and expand.
{queryParam=:other-parm} -> {queryParam=**:otherparam} // Note: Default cardinality changes between match and expand.
{queryParam=*:other-parm} -> {queryParam=*:otherparam}
{queryParam=**:other-parm} -> {queryParam=**:otherparam}
*/
public class Matcher<V> {
private Map<Template,V> map;
private PathNode root;
public Matcher() {
map = new LinkedHashMap<>();
root = new PathNode( null, null );
}
public Matcher( Template template, V value ) {
this();
add( template, value );
}
public V get( Template template ) {
return map.get( template );
}
public void add( Template template, V value ) {
map.put( template, value );
PathNode node = root;
// Add the scheme segment to the tree (if any) while descending.
node = add( node, template.getScheme() );
// Add the authority segments (if any) while descending.
node = add( node, template.getUsername() );
node = add( node, template.getPassword() );
node = add( node, template.getHost() );
node = add( node, template.getPort() );
// Add the path segments while descending.
for( Path segment : template.getPath() ) {
// If the root already contains a matching segment then descend to that pathNode.
// Otherwise create a child pathNode, addValue it to the root and descend to it.
// If this new pathNode is a leaf pathNode then set the value.
node = add( node, segment );
}
// Add the fragment (if any) segments while descending.
// Note: Doing it this way puts the fragment above the query parameters in the match order.
node = add( node, template.getFragment() );
if( template.getQuery().isEmpty() && template.getExtra() == null ) {
// The first template with a value at this node wins.
if( node.value == null ) {
node.template = template;
node.value = value;
}
} else {
// Insert a query pathNode into the tree.
node.addQuery( template, value );
}
}
private PathNode add( PathNode parent, Segment segment ) {
PathNode child = parent;
if( segment != null ) {
if( ( parent.children != null ) && ( parent.children.containsKey( segment ) ) ) {
child = parent.children.get( segment );
} else {
child = parent.addPath( segment );
}
}
return child;
}
public Match match( Template input ) {
Status status = new Status();
status.candidates.add( new MatchSegment( null, root, null, null ) );
boolean matches = true;
// Separate &= statements for debugability.
matches &= matchScheme( input, status );
matches &= matchAuthority( input, status );
matches &= matchPath( input, status );
matches &= matchFragment( input, status );
Match winner;
if( matches ) {
winner = pickBestMatch( input, status );
} else {
winner = null;
}
return winner;
}
private boolean matchScheme( Template input, Status status ) {
pickMatchingChildren( input.getScheme(), status );
return status.hasCandidates();
}
private boolean matchAuthority( Template input, Status status ) {
pickMatchingChildren( input.getUsername(), status );
pickMatchingChildren( input.getPassword(), status );
pickMatchingChildren( input.getHost(), status );
// port does not makes sense without host
if(input.getHost() != null) {
// port is optional, since default ports do not need to present in URL
pickMatchingOptionalSegment(input.getPort(), status);
}
return status.hasCandidates();
}
private boolean matchPath( Template input, Status status ) {
Path segment;
Iterator<Path> segments = input.getPath().iterator();
while( segments.hasNext() && status.hasCandidates() ) {
segment = segments.next();
pickMatchingChildren( segment, status );
}
return status.hasCandidates();
}
private boolean matchFragment( Template input, Status status ) {
pickMatchingChildren( input.getFragment(), status );
return status.hasCandidates();
}
private void pickMatchingChildren( Segment segment, Status status ) {
if( segment != null ) {
for( MatchSegment parent : status.candidates ) {
if( parent.pathNode.hasGlob() ) {
status.matches.add( new MatchSegment( parent, parent.pathNode, parent.pathNode.segment, segment ) );
}
if( parent.pathNode.children != null ) {
for( PathNode node : parent.pathNode.children.values() ) {
if( node.matches( segment ) ) {
status.matches.add( new MatchSegment( parent, node, node.segment, segment ) );
}
}
}
}
status.swapMatchesToCandidates();
}
}
/**
* optional segment, if it does not present (it is null) it is accepted
*/
private void pickMatchingOptionalSegment( Segment segment, Status status ) {
for( MatchSegment parent : status.candidates ) {
if( parent.pathNode.children != null ) {
for( PathNode node : parent.pathNode.children.values() ) {
if( segment != null ) {
if( node.matches( segment ) ) {
status.matches.add( new MatchSegment( parent, node, node.segment, segment ) );
}
} else {
status.matches.add( new MatchSegment( parent, node, node.segment, segment ) );
}
}
}
}
status.swapMatchesToCandidates();
}
private Match pickBestMatch( Template input, Status status ) {
Match bestMatch = new Match( null, null );
PathNode bestPath = null;
QueryNode bestQuery = null;
MatchSegment bestMatchSegment = null;
for( MatchSegment matchSegment: status.candidates ) {
PathNode pathNode = matchSegment.pathNode;
if( ( bestPath == null ) || // If we don't have anything at all pick the pathNode.
( pathNode.depth > bestPath.depth ) || // If the pathNode is deeper than the best pathNode, pick it.
// If the pathNode is the same depth as current best but is static and the best isn't then pick it.
( ( pathNode.depth == bestPath.depth ) && ( pathNode.getType() < bestPath.getType() ) ) ) {
// If the path node has a template then assume we will pick the path node.
if( pathNode.template != null ) {
bestPath = pathNode;
bestQuery = null;
bestMatch.template = pathNode.template;
bestMatch.value = pathNode.value;
bestMatchSegment = matchSegment;
}
// If the path node has queries see if one is better match than the path node itself.
if( pathNode.hasQueries() ) {
bestQuery = pickBestQueryMatch( input, pathNode );
if( bestQuery != null && bestQuery.template != null ) {
bestPath = pathNode;
bestMatch.template = bestQuery.template;
bestMatch.value = bestQuery.value;
bestMatchSegment = matchSegment;
}
}
}
}
Match match = createMatch( bestMatchSegment, bestPath, bestQuery, input );
return match;
}
private QueryNode pickBestQueryMatch( Template input, PathNode pathNode ) {
QueryNode bestNode = null;
int bestMatchCount = 0;
for( QueryNode node : pathNode.queries ) {
Query extra = node.template.getExtra();
int nodeQuerySize = node.template.getQuery().size();
int queryMatchCount = calcQueryMatchCount( node, input );
boolean matchesNamedQueries = queryMatchCount >= nodeQuerySize;
boolean matchesExtraQuery =
( ( extra == null ) ||
( Segment.GLOB_PATTERN.equals( extra.getQueryName() ) ) ||
( input.getQuery().size() > nodeQuerySize ) );
if( ( bestNode == null || queryMatchCount > bestMatchCount ) && ( matchesNamedQueries && matchesExtraQuery ) ) {
bestMatchCount = queryMatchCount;
bestNode = node;
}
}
return bestNode;
}
private int calcQueryMatchCount( QueryNode node, Template input ) {
int matchCount = 0;
Map<String,Query> inputQuery = input.getQuery();
Map<String,Query> templateQuery = node.template.getQuery();
for( Query templateSegment : templateQuery.values() ) {
Query inputSegment = inputQuery.get( templateSegment.getQueryName() );
if( inputSegment != null && templateSegment.matches( inputSegment ) ) {
matchCount++ ;
} else {
matchCount = 0;
break;
}
}
return matchCount;
}
private Match createMatch( MatchSegment bestMatchSegment, PathNode bestPath, QueryNode bestQuery, Template input ) {
Match match = null;
if( bestPath != null ) { //&& ( bestQuery != null || !bestPath.hasQueries() ) ) {
if( bestQuery != null ) {
match = new Match( bestQuery.template, bestQuery.value );
} else {
match = new Match( bestPath.template, bestPath.value );
}
MatchParams matchParams = new MatchParams();
// Add the matching query segments to the end of the list.
if( bestQuery != null ) {
Map<String,Query> inputQuery = input.getQuery();
for( Query templateSegment : bestQuery.template.getQuery().values() ) {
Query inputSegment = inputQuery.get( templateSegment.getQueryName() );
if( inputSegment != null && templateSegment.matches( inputSegment ) ) {
extractSegmentParams( templateSegment, inputSegment, matchParams );
}
}
}
// If the template has the "extra" query queryParam then collect query params that were
// not already matched.
if( bestQuery != null ) {
Query extra = bestQuery.template.getExtra();
if( extra != null ) {
String paramName = extra.getParamName();
if( paramName != null && paramName.length() > 0 ) {
for( Query query: input.getQuery().values() ) {
String queryName = query.getQueryName();
if( matchParams.resolve( queryName ) == null ) {
for( Segment.Value value: query.getValues() ) {
matchParams.addValue( queryName, value.getEffectivePattern() );
}
}
}
}
}
}
// Walk back up the matching segment tree.
MatchSegment matchSegment = bestMatchSegment;
while( matchSegment != null && matchSegment.pathNode.depth > 0 ) {
extractSegmentParams( matchSegment.templateSegment, matchSegment.inputSegment, matchParams );
matchSegment = matchSegment.parentMatch;
}
match.params = matchParams;
}
return match;
}
private void extractSegmentParams( Segment extractSegment, Segment inputSegment, MatchParams params ) {
if( extractSegment != null && inputSegment != null ) {
String paramName = extractSegment.getParamName();
if( paramName.length() > 0 ) {
for( Segment.Value value: inputSegment.getValues() ) {
params.insertValue( paramName, value.getEffectivePattern() );
}
}
}
}
private class Status {
List<MatchSegment> candidates = new ArrayList<>();
List<MatchSegment> matches = new ArrayList<>();
List<MatchSegment> temp;
private void swapMatchesToCandidates() {
temp = candidates; candidates = matches; matches = temp;
matches.clear();
}
private boolean hasCandidates() {
return !candidates.isEmpty();
}
}
private class MatchSegment {
private MatchSegment parentMatch;
private PathNode pathNode;
private Segment templateSegment;
private Segment inputSegment;
private MatchSegment( MatchSegment parent, PathNode node, Segment templateSegment, Segment inputSegment ) {
this.parentMatch = parent;
this.pathNode = node;
this.templateSegment = templateSegment;
this.inputSegment = inputSegment;
}
}
private class MatchParams implements Params {
private Map<String,List<String>> map = new HashMap<>();
public Set<String> getNames() {
return map.keySet();
}
private List<String> getOrAddValues( String name ) {
List<String> values = resolve( name );
if( values == null ) {
values = new ArrayList<>( 1 );
map.put( name, values );
}
return values;
}
public void addValue( String name, String value ) {
List<String> values = getOrAddValues( name );
values.add( value );
}
public void insertValue( String name, String value ) {
List<String> values = getOrAddValues( name );
values.add( 0, value );
}
@Override
public List<String> resolve( String name ) {
return map.get( name );
}
}
public class Match {
private Template template;
private V value;
private Params params;
//TODO private Params extra;
private Match( Template template, V value ) {
this.template = template;
this.value = value;
}
public Template getTemplate() {
return template;
}
public V getValue() {
return value;
}
public Params getParams() {
return params;
}
}
private class PathNode extends Node {
int depth; // Zero based depth of the pathNode for "best pathNode" calculation.
Segment segment;
Map<Segment,PathNode> children;
Set<QueryNode> queries;
private PathNode( PathNode parent, Segment segment ) {
super( null, null );
this.depth = ( parent == null ) ? 0 : parent.depth+1;
this.segment = segment;
this.children = null;
this.queries = null;
}
private PathNode addPath( Segment path ) {
if( children == null ) {
children = new LinkedHashMap<>();
}
PathNode child = new PathNode( this, path );
children.put( path, child );
return child;
}
private QueryNode addQuery( Template template, V value ) {
if( queries == null ) {
queries = new LinkedHashSet<>();
}
QueryNode query = new QueryNode( template, value );
queries.add( query );
return query;
}
private int getType() {
int type = Segment.UNKNOWN;
if( segment != null ) {
for( Segment.Value value: segment.getValues() ) {
int vType = value.getType();
type = type < vType ? type : vType;
if( type == Segment.STATIC ) {
break;
}
}
}
return type;
}
private boolean hasGlob() {
boolean is = false;
if( segment != null ) {
for( Segment.Value value: segment.getValues() ) {
if( Segment.GLOB == value.getType() ) {
is = true;
}
}
}
return is;
}
private boolean hasQueries() {
return( queries != null && !queries.isEmpty() );
}
private boolean matches( Segment segment ) {
return( this.segment.matches( segment ) );
}
}
private class QueryNode extends Node {
private QueryNode( Template template, V value ) {
super( template, value );
}
}
private class Node {
Template template;
V value;
private Node( Template template, V value ) {
this.template = template;
this.value = value;
}
}
}