Skip to content

Commit

Permalink
Merge 7034579 into aefdfe9
Browse files Browse the repository at this point in the history
  • Loading branch information
decebals committed Sep 28, 2017
2 parents aefdfe9 + 7034579 commit 64b1a4a
Show file tree
Hide file tree
Showing 8 changed files with 503 additions and 228 deletions.
13 changes: 7 additions & 6 deletions pippo-core/src/main/java/ro/pippo/core/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import ro.pippo.core.util.MimeTypes;
import ro.pippo.core.util.ServiceLocator;
import ro.pippo.core.websocket.WebSocketHandler;
import ro.pippo.core.websocket.WebSocketRouter;

import javax.servlet.ServletContext;
import java.util.ArrayList;
Expand Down Expand Up @@ -74,7 +75,7 @@ public class Application implements ResourceRouting {
private Map<String, Object> locals;
private RouteHandler notFoundRouteHandler;

private Map<String, WebSocketHandler> webSocketHandlers;
private WebSocketRouter webSocketRouter;

public Application() {
this(new PippoSettings(RuntimeMode.getCurrent()));
Expand All @@ -88,7 +89,7 @@ public Application(PippoSettings settings) {
this.httpCacheToolkit = new HttpCacheToolkit(settings);
this.engines = new ContentTypeEngines();
this.initializers = new ArrayList<>();
this.webSocketHandlers = new HashMap<>();
this.webSocketRouter = new WebSocketRouter();

registerContentTypeEngine(TextPlainEngine.class);
}
Expand Down Expand Up @@ -375,12 +376,12 @@ public RouteHandler getNotFoundRouteHandler() {
return notFoundRouteHandler;
}

public void addWebSocket(String path, WebSocketHandler webSocketHandler) {
webSocketHandlers.put(path, webSocketHandler);
public void addWebSocket(String uriPattern, WebSocketHandler webSocketHandler) {
webSocketRouter.addRoute(uriPattern, webSocketHandler);
}

public WebSocketHandler getWebSocketHandler(String path) {
return webSocketHandlers.get(path);
public WebSocketRouter getWebSocketRouter() {
return webSocketRouter;
}

/**
Expand Down
267 changes: 267 additions & 0 deletions pippo-core/src/main/java/ro/pippo/core/DefaultUriMatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/*
* Copyright (C) 2017 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 ro.pippo.core;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author Decebal Suiu
*/
public class DefaultUriMatcher implements UriMatcher {

private static final Logger log = LoggerFactory.getLogger(DefaultUriMatcher.class);

// Matches: {id} AND {id: .*?}
// group(1) extracts the name of the group (in that case "id").
// group(3) extracts the regex if defined
private static final Pattern PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE = Pattern.compile("\\{(.*?)(:\\s(.*?))?\\}");

// This regex matches everything in between path slashes.
private static final String VARIABLE_ROUTES_DEFAULT_REGEX = "(?<%s>[^/]+)";

// This regex works for both {myParam} AND {myParam: .*}
private static final String VARIABLE_PART_PATTERN_WITH_PLACEHOLDER = "\\{(%s)(:\\s([^}]*))?\\}";

private static final String PATH_PARAMETER_REGEX_GROUP_NAME_PREFIX = "param";

// key = uri pattern
private Map<String, UriPatternBinding> bindings;

public DefaultUriMatcher() {
bindings = new HashMap<>();
}

@Override
public Map<String, String> match(String requestUri, String uriPattern) {
UriPatternBinding binding = bindings.get(uriPattern);
if (binding == null) {
// something is wrong
throw new PippoRuntimeException("No binding for '{}'. Create binding with 'addUriPattern'.", uriPattern);
}

return binding.getPattern().matcher(requestUri).matches() ? getParameters(binding, requestUri) : null;
}

@Override
public UriPatternBinding addUriPattern(String uriPattern) {
if (bindings.containsKey(uriPattern)) {
return bindings.get(uriPattern);
}

String regex = getRegex(uriPattern);
Pattern pattern = Pattern.compile(regex);
List<String> parameterNames = getParameterNames(uriPattern);
UriPatternBinding binding = new UriPatternBinding(uriPattern, pattern, parameterNames);
bindings.put(uriPattern, binding);
log.debug("Add binding '{}'", binding);

return binding;
}

@Override
public UriPatternBinding removeUriPattern(String uriPattern) {
return bindings.remove(uriPattern);
}

@Override
public String uriFor(Map<String, Object> parameters, String uriPattern) {
UriPatternBinding binding = bindings.get(uriPattern);
if (binding == null) {
// something is wrong
throw new PippoRuntimeException("No binding for '{}'. Create binding with 'addUriPattern'.", uriPattern);
}

List<String> parameterNames = binding.getParameterNames();
if (!parameters.keySet().containsAll(parameterNames)) {
log.error("You must provide values for all path parameters. {} vs {}", parameterNames, parameters.keySet());
}

Map<String, Object> queryParameters = new HashMap<>(parameters.size());

// create a uri starting from uriPattern (that can contains path params placeholders)
String uri = binding.getUriPattern();

// replace path params placeholders from uri pattern
for (Map.Entry<String, Object> parameterPair : parameters.entrySet()) {
boolean foundAsPathParameter = false;

StringBuffer sb = new StringBuffer();
String regex = String.format(VARIABLE_PART_PATTERN_WITH_PLACEHOLDER, parameterPair.getKey());
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(uri);
while (matcher.find()) {
matcher.appendReplacement(sb, getPathParameterValue(uriPattern, parameterPair.getKey(), parameterPair.getValue()));
foundAsPathParameter = true;
}

matcher.appendTail(sb);
uri = sb.toString();

if (!foundAsPathParameter) {
queryParameters.put(parameterPair.getKey(), parameterPair.getValue());
}
}

// prepare the query string for this url if we got some query params
if (!queryParameters.isEmpty()) {
// add remaining parameters as query parameters
StringBuilder query = new StringBuilder();
Iterator<Map.Entry<String, Object>> iterator = queryParameters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> parameterEntry = iterator.next();
String parameterName = parameterEntry.getKey();
Object parameterValue = parameterEntry.getValue();
String encodedParameterValue;
try {
encodedParameterValue = URLEncoder.encode(parameterValue.toString(), PippoConstants.UTF8);
} catch (UnsupportedEncodingException e) {
throw new PippoRuntimeException(e, "Cannot encode the parameter value '{}'", parameterValue.toString());
}
query.append(parameterName).append("=").append(encodedParameterValue);

if (iterator.hasNext()) {
query.append("&");
}
}

uri += "?" + query;
}

return uri;
}

protected String getPathParameterValue(String uriPattern, String parameterName, Object parameterValue) {
return parameterValue.toString();
}

@SuppressWarnings("unchecked")
private Map<String, String> getParameters(UriPatternBinding binding, String requestUri) {
List<String> parameterNames = binding.getParameterNames();
if (parameterNames.isEmpty()) {
return Collections.EMPTY_MAP;
}

Map<String, String> parameters = new HashMap<>();
Matcher matcher = binding.getPattern().matcher(requestUri);
matcher.matches(); // always true
int groupCount = matcher.groupCount();
if (groupCount > 0) {
for (int i = 0; i < parameterNames.size(); i++) {
parameters.put(parameterNames.get(i), matcher.group(getPathParameterRegexGroupName(i)));
}
}

return parameters;
}

/**
* Transforms an url pattern like "/{name}/id/*" into a regex like "/([^/]*)/id/*."
* <p/>
* Also handles regular expressions if defined inside routes:
* For instance "/users/{username: [a-zA-Z][a-zA-Z_0-9]}" becomes
* "/users/([a-zA-Z][a-zA-Z_0-9])"
*
* @return The converted regex with default matching regex - or the regex
* specified by the user.
*/
private String getRegex(String urlPattern) {
StringBuffer buffer = new StringBuffer();

Matcher matcher = PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE.matcher(urlPattern);
int pathParameterIndex = 0;
while (matcher.find()) {
// By convention group 3 is the regex if provided by the user.
// If it is not provided by the user the group 3 is null.
String namedVariablePartOfRoute = matcher.group(3);
String namedVariablePartOfORouteReplacedWithRegex;

if (namedVariablePartOfRoute != null) {
// we convert that into a regex matcher group itself
String variableRegex = replacePosixClasses(namedVariablePartOfRoute);
namedVariablePartOfORouteReplacedWithRegex = String.format("(?<%s>%s)",
getPathParameterRegexGroupName(pathParameterIndex), Matcher.quoteReplacement(variableRegex));
} else {
// we convert that into the default namedVariablePartOfRoute regex group
namedVariablePartOfORouteReplacedWithRegex = String.format(VARIABLE_ROUTES_DEFAULT_REGEX,
getPathParameterRegexGroupName(pathParameterIndex));
}
// we replace the current namedVariablePartOfRoute group
matcher.appendReplacement(buffer, namedVariablePartOfORouteReplacedWithRegex);
pathParameterIndex++;
}

// .. and we append the tail to complete the stringBuffer
matcher.appendTail(buffer);

return buffer.toString();
}

private String getPathParameterRegexGroupName(int pathParameterIndex) {
return PATH_PARAMETER_REGEX_GROUP_NAME_PREFIX + pathParameterIndex;
}

/**
* Replace any specified POSIX character classes with the Java equivalent.
*
* @param input
* @return a Java regex
*/
private String replacePosixClasses(String input) {
return input
.replace(":alnum:", "\\p{Alnum}")
.replace(":alpha:", "\\p{L}")
.replace(":ascii:", "\\p{ASCII}")
.replace(":digit:", "\\p{Digit}")
.replace(":xdigit:", "\\p{XDigit}");
}

/**
* Extracts the name of the parameters from a route
* <p/>
* /{my_id}/{my_name}
* <p/>
* would return a List with "my_id" and "my_name"
*
* @param uriPattern
* @return a list with the names of all parameters in the url pattern
*/
private List<String> getParameterNames(String uriPattern) {
List<String> list = new ArrayList<>();

Matcher matcher = PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE.matcher(uriPattern);
while (matcher.find()) {
// group(1) is the name of the group. Must be always there...
// "/assets/{file}" and "/assets/{file: [a-zA-Z][a-zA-Z_0-9]}"
// will return file.
list.add(matcher.group(1));
}

return list;
}

}
78 changes: 78 additions & 0 deletions pippo-core/src/main/java/ro/pippo/core/UriMatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2017 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 ro.pippo.core;

import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
* @author Decebal Suiu
*/
public interface UriMatcher {

/**
* Returns null for no matching and not null for matching.
* The returned map represent the path parameters.
*
* @param requestUri
* @param uriPattern
* @return
*/
Map<String, String> match(String requestUri, String uriPattern);

UriPatternBinding addUriPattern(String uriPattern);

UriPatternBinding removeUriPattern(String uriPattern);

String uriFor(Map<String, Object> parameters, String uriPattern);

class UriPatternBinding {

private final String uriPattern;
private final Pattern pattern;
private final List<String> parameterNames;

public UriPatternBinding(String uriPattern, Pattern pattern, List<String> parameterNames) {
this.uriPattern = uriPattern;
this.pattern = pattern;
this.parameterNames = parameterNames;
}

public String getUriPattern() {
return uriPattern;
}

public Pattern getPattern() {
return pattern;
}

public List<String> getParameterNames() {
return parameterNames;
}

@Override
public String toString() {
return "UriPatternBinding{" +
"uriPattern=" + uriPattern +
", pattern=" + pattern +
", parameterNames=" + parameterNames +
'}';
}

}

}

0 comments on commit 64b1a4a

Please sign in to comment.