/*
* Copyright 2012 Jason Miller
*
* 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 jj.http.server.uri;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* <p>
* Inspects a route URI string to validate that it is legal
* <ol>
* <li>Starts with /
* <li>is composed of segments separated by /
* <li>segments match either
* </ol>
*
* <p>
* This complicates the usage a little bit, since in principle you should
* call the validator before creating a route, but it's set up this way
* for the javascript API, because throwing exceptions makes that all
* weird and hard to write
* @author jason
*
*/
@Singleton
public class RouteUriValidator {
private static final Pattern SPLITTER = Pattern.compile("/");
static final Pattern STATIC_NODE_PATTERN = Pattern.compile("^[a-zA-Z0-9.\\+%-]*$");
static final Pattern PARAMETER_NAME_PATTERN = Pattern.compile("^[:\\*][a-zA-Z$_][a-zA-Z0-9$_]*$");
@Inject
RouteUriValidator() {}
private void append(StringBuilder errorBuilder, String...errors) {
if (errorBuilder.length() > 0) {
errorBuilder.append("\n");
}
for (String error : errors) {
errorBuilder.append(error);
}
}
private void validateStaticSegment(String segment, StringBuilder errors) {
if (!STATIC_NODE_PATTERN.matcher(segment).matches()) {
append(errors, "segment ", segment, " contains invalid URL characters");
}
}
private void validateParameter(String segment, StringBuilder errors) {
int patternStart = segment.indexOf('(');
String pattern = "";
String toCheck = segment;
if (patternStart > -1) {
pattern = segment.substring(patternStart);
toCheck = segment.substring(0, patternStart);
}
if (!PARAMETER_NAME_PATTERN.matcher(toCheck).matches()) {
append(errors, "parameter ", segment, " must have a valid JavaScript variable name");
}
if (!pattern.isEmpty()) {
if (!pattern.endsWith(")")) {
append(errors, "parameter ", segment, " has an invalid pattern specification ", pattern);
} else {
try {
Pattern.compile(pattern.substring(1, pattern.length() - 1));
} catch (PatternSyntaxException pse) {
append(errors, "parameter ", segment ," pattern ", pattern.substring(1, pattern.length() - 1), " failed to compile");
append(errors, pse.getMessage());
}
}
}
}
private void validateParamSegment(String segment, StringBuilder errors) {
validateParameter(segment, errors);
}
private void validateSplatSegment(String segment, boolean last, StringBuilder errors) {
if (!last) {
errors.append("* parameter must be the last path segment in a uri");
}
validateParameter(segment, errors);
}
private void doValidateSegment(String segment, boolean last, StringBuilder errors) {
if (segment.startsWith("*")) {
validateSplatSegment(segment, last, errors);
} else if (segment.startsWith(":")) {
validateParamSegment(segment, errors);
} else {
validateStaticSegment(segment, errors);
}
}
private void validateSegment(String segment, boolean last, StringBuilder errors) {
String toValidate = segment;
String extension = null;
if (last) {
// split off any extension found
int end = segment.lastIndexOf('.');
if (end > -1) {
toValidate = segment.substring(0, end);
extension = segment.substring(end + 1);
}
}
doValidateSegment(toValidate, last, errors);
if (extension != null) {
doValidateSegment(extension, last, errors);
}
}
private void validate(String[] segments, StringBuilder errors) {
for (int i = 0; i < segments.length; ++i) {
validateSegment(segments[i], i == segments.length - 1, errors);
}
}
/**
* Validates a route URI, returning as many errors as can be discovered
* as a \n separated string. if the returned string is empty, it's all
* good
*/
public String validateRouteUri(final String uri) {
StringBuilder errors = new StringBuilder();
if (uri == null) {
append(errors, "uri must not be null");
}
else if (uri.isEmpty() || uri.charAt(0) != '/') {
append(errors, "uri must start with /");
validate(SPLITTER.split(uri), errors);
}
else {
validate(SPLITTER.split(uri.substring(1)), errors);
}
return errors.toString();
}
}