/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.util;
import io.undertow.
UndertowMessages;
import java.util.
ArrayList;
import java.util.
Collections;
import java.util.
HashSet;
import java.util.
List;
import java.util.
Map;
import java.util.
Set;
/**
* Represents a parsed web socket path template.
* <p>
* This class can be compared to other path templates, with templates that are considered
* lower have a higher priority, and should be checked first.
* <p>
* This comparison can also be used to check for semantically equal paths, if
* a.compareTo(b) == 0 then the two paths are equivalent, which will generally
* result in a deployment exception.
*
* @author Stuart Douglas
*/
public class
PathTemplate implements
Comparable<
PathTemplate> {
private final
String templateString;
private final boolean
template;
private final
String base;
final
List<
Part>
parts;
private final
Set<
String>
parameterNames;
private final boolean
trailingSlash;
private
PathTemplate(
String templateString, final boolean
template, final
String base, final
List<
Part>
parts,
Set<
String>
parameterNames, boolean
trailingSlash) {
this.
templateString =
templateString;
this.
template =
template;
this.
base =
base;
this.
parts =
parts;
this.
parameterNames =
Collections.
unmodifiableSet(
parameterNames);
this.
trailingSlash =
trailingSlash;
}
public static
PathTemplate create(final
String inputPath) {
// a path is required
if(
inputPath == null) {
throw
UndertowMessages.
MESSAGES.
pathMustBeSpecified();
}
// prepend a "/" if none is present
if(!
inputPath.
startsWith("/")) {
return
PathTemplate.
create("/" +
inputPath);
}
// create string from modified string
final
String path =
inputPath;
int
state = 0;
String base = "";
List<
Part>
parts = new
ArrayList<>();
int
stringStart = 0;
//0 parsing base
//1 parsing base, last char was /
//2 in template part
//3 just after template part, expecting /
//4 expecting either template or segment
//5 in segment
for (int
i = 0;
i <
path.
length(); ++
i) {
final int
c =
path.
charAt(
i);
switch (
state) {
case 0: {
if (
c == '/') {
state = 1;
} else if (
c == '*') {
base =
path.
substring(0,
i + 1);
stringStart =
i;
state = 5;
} else {
state = 0;
}
break;
}
case 1: {
if (
c == '{') {
base =
path.
substring(0,
i);
stringStart =
i + 1;
state = 2;
} else if (
c == '*') {
base =
path.
substring(0,
i + 1);
stringStart =
i;
state = 5;
} else if (
c != '/') {
state = 0;
}
break;
}
case 2: {
if (
c == '}') {
Part part = new
Part(true,
path.
substring(
stringStart,
i));
parts.
add(
part);
stringStart =
i;
state = 3;
}
break;
}
case 3: {
if (
c == '/') {
state = 4;
} else {
throw
UndertowMessages.
MESSAGES.
couldNotParseUriTemplate(
path,
i);
}
break;
}
case 4: {
if (
c == '{') {
stringStart =
i + 1;
state = 2;
} else if (
c != '/') {
stringStart =
i;
state = 5;
}
break;
}
case 5: {
if (
c == '/') {
Part part = new
Part(false,
path.
substring(
stringStart,
i));
parts.
add(
part);
stringStart =
i + 1;
state = 4;
}
break;
}
}
}
boolean
trailingSlash = false;
switch (
state) {
case 1:
trailingSlash = true;
//fall through
case 0: {
base =
path;
break;
}
case 2: {
throw
UndertowMessages.
MESSAGES.
couldNotParseUriTemplate(
path,
path.
length());
}
case 4: {
trailingSlash = true;
break;
}
case 5: {
Part part = new
Part(false,
path.
substring(
stringStart));
parts.
add(
part);
break;
}
}
final
Set<
String>
templates = new
HashSet<>();
for(
Part part :
parts) {
if(
part.
template) {
templates.
add(
part.
part);
}
}
return new
PathTemplate(
path,
state > 1 && !
base.
contains("*"),
base,
parts,
templates,
trailingSlash);
}
/**
* Check if the given uri matches the template. If so then it will return true and
* place the value of any path parameters into the given map.
* <p>
* Note the map may be modified even if the match in unsuccessful, however in this case
* it will be emptied before the method returns
*
* @param path The request path, relative to the context root
* @param pathParameters The path parameters map to fill out
* @return true if the URI is a match
*/
public boolean
matches(final
String path, final
Map<
String,
String>
pathParameters) {
if (!
template &&
base.
contains("*")) {
final int
indexOf =
base.
indexOf("*");
final
String startBase =
base.
substring(0,
indexOf);
if (!
path.
startsWith(
startBase)) {
return false;
}
pathParameters.
put("*",
path.
substring(
indexOf,
path.
length()));
return true;
}
if (!
path.
startsWith(
base)) {
return false;
}
int
baseLength =
base.
length();
if (!
template) {
return
path.
length() ==
baseLength;
}
if(
trailingSlash) {
//the template has a trailing slash
//we verify this first as it is cheap
//and it simplifies the matching algorithm below
if(
path.
charAt(
path.
length() -1 ) != '/') {
return false;
}
}
int
currentPartPosition = 0;
PathTemplate.
Part current =
parts.
get(
currentPartPosition);
int
stringStart =
baseLength;
int
i;
for (
i =
baseLength;
i <
path.
length(); ++
i) {
final char
currentChar =
path.
charAt(
i);
if (
currentChar == '?' ||
current.
part.
equals("*")) {
break;
} else if (
currentChar == '/') {
String result =
path.
substring(
stringStart,
i);
if (
current.
template) {
pathParameters.
put(
current.
part,
result);
} else if (!
result.
equals(
current.
part)) {
pathParameters.
clear();
return false;
}
++
currentPartPosition;
if (
currentPartPosition ==
parts.
size()) {
//this is a match if this is the last character
return
i == (
path.
length() - 1);
}
current =
parts.
get(
currentPartPosition);
stringStart =
i + 1;
}
}
if (
currentPartPosition + 1 !=
parts.
size()) {
pathParameters.
clear();
return false;
}
String result =
path.
substring(
stringStart,
i);
if (
current.
part.
equals("*")) {
pathParameters.
put(
current.
part,
path.
substring(
stringStart,
path.
length()));
return true;
}
if (
current.
template) {
pathParameters.
put(
current.
part,
result);
} else if (!
result.
equals(
current.
part)) {
pathParameters.
clear();
return false;
}
return true;
}
@
Override
public boolean
equals(
Object o) {
if (this ==
o) return true;
if (!(
o instanceof
PathTemplate)) return false;
PathTemplate that = (
PathTemplate)
o;
return this.
compareTo(
that) == 0;
}
@
Override
public int
hashCode() {
int
result =
getTemplateString() != null ?
getTemplateString().
hashCode() : 0;
result = 31 *
result + (
template ? 1 : 0);
result = 31 *
result + (
getBase() != null ?
getBase().
hashCode() : 0);
result = 31 *
result + (
parts != null ?
parts.
hashCode() : 0);
result = 31 *
result + (
getParameterNames() != null ?
getParameterNames().
hashCode() : 0);
return
result;
}
@
Override
public int
compareTo(final
PathTemplate o) {
//we want templates with the highest priority to sort first
//so we sort in reverse priority order
//templates have lower priority
if (
template && !
o.
template) {
return 1;
} else if (
o.
template && !
template) {
return -1;
}
int
res =
base.
compareTo(
o.
base);
if (
res > 0) {
//our base is longer
return -1;
} else if (
res < 0) {
return 1;
} else if (!
template) {
//they are the same path
return 0;
}
//the first path with a non-template element
int
i = 0;
for (; ; ) {
if (
parts.
size() ==
i) {
if (
o.
parts.
size() ==
i) {
return
base.
compareTo(
o.
base);
}
return 1;
} else if (
o.
parts.
size() ==
i) {
//we have more parts, so should be checked first
return -1;
}
Part thisPath =
parts.
get(
i);
Part otherPart =
o.
parts.
get(
i);
if (
thisPath.
template && !
otherPart.
template) {
//non template part sorts first
return 1;
} else if (!
thisPath.
template &&
otherPart.
template) {
return -1;
} else if (!
thisPath.
template) {
int
r =
thisPath.
part.
compareTo(
otherPart.
part);
if (
r != 0) {
return
r;
}
}
++
i;
}
}
public
String getBase() {
return
base;
}
public
String getTemplateString() {
return
templateString;
}
public
Set<
String>
getParameterNames() {
return
parameterNames;
}
private static class
Part {
final boolean
template;
final
String part;
private
Part(final boolean
template, final
String part) {
this.
template =
template;
this.
part =
part;
}
@
Override
public
String toString() {
return "Part{" +
"template=" +
template +
", part='" +
part + '\'' +
'}';
}
}
@
Override
public
String toString() {
return "PathTemplate{" +
"template=" +
template +
", base='" +
base + '\'' +
", parts=" +
parts +
'}';
}
}