Thu, 15 Oct 2020 18:36:05 +0200
adds the possibility to specify path parameters to RequestMapping
1.1 --- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Thu Oct 15 14:01:49 2020 +0200 1.2 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Thu Oct 15 18:36:05 2020 +0200 1.3 @@ -84,7 +84,7 @@ 1.4 * The reason for this is the different handling of empty paths in 1.5 * {@link HttpServletRequest#getPathInfo()}. 1.6 */ 1.7 - private final Map<HttpMethod, Map<String, Method>> mappings = new HashMap<>(); 1.8 + private final Map<HttpMethod, Map<PathPattern, Method>> mappings = new HashMap<>(); 1.9 1.10 /** 1.11 * Returns the name of the resource bundle associated with this servlet. 1.12 @@ -108,7 +108,9 @@ 1.13 throw new AssertionError("Non-exhaustive if-else - this is a bug."); 1.14 } 1.15 1.16 - private ResponseType invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException { 1.17 + private ResponseType invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException { 1.18 + final var pathPattern = mapping.getKey(); 1.19 + final var method = mapping.getValue(); 1.20 try { 1.21 LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName()); 1.22 final var paramTypes = method.getParameterTypes(); 1.23 @@ -122,6 +124,9 @@ 1.24 if (paramTypes[i].isAssignableFrom(DataAccessObjects.class)) { 1.25 paramValues[i] = dao; 1.26 } 1.27 + if (paramTypes[i].isAssignableFrom(PathParameters.class)) { 1.28 + paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req)); 1.29 + } 1.30 } 1.31 return (ResponseType) method.invoke(this, paramValues); 1.32 } catch (InvocationTargetException ex) { 1.33 @@ -152,6 +157,13 @@ 1.34 for (Method method : methods) { 1.35 Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class)); 1.36 if (mapping.isPresent()) { 1.37 + if (mapping.get().requestPath().isBlank()) { 1.38 + LOG.warn("{} is annotated with {} but request path is empty", 1.39 + method.getName(), RequestMapping.class.getSimpleName() 1.40 + ); 1.41 + continue; 1.42 + } 1.43 + 1.44 if (!Modifier.isPublic(method.getModifiers())) { 1.45 LOG.warn("{} is annotated with {} but is not public", 1.46 method.getName(), RequestMapping.class.getSimpleName() 1.47 @@ -175,28 +187,35 @@ 1.48 for (var param : method.getParameterTypes()) { 1.49 paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param) 1.50 || HttpServletResponse.class.isAssignableFrom(param) 1.51 + || PathParameters.class.isAssignableFrom(param) 1.52 || DataAccessObjects.class.isAssignableFrom(param); 1.53 } 1.54 if (paramsInjectible) { 1.55 - String requestPath = "/" + mapping.get().requestPath(); 1.56 + try { 1.57 + PathPattern pathPattern = new PathPattern(mapping.get().requestPath()); 1.58 1.59 - if (mappings 1.60 - .computeIfAbsent(mapping.get().method(), k -> new HashMap<>()) 1.61 - .putIfAbsent(requestPath, method) != null) { 1.62 - LOG.warn("{} {} has multiple mappings", 1.63 + if (mappings 1.64 + .computeIfAbsent(mapping.get().method(), k -> new HashMap<>()) 1.65 + .putIfAbsent(pathPattern, method) != null) { 1.66 + LOG.warn("{} {} has multiple mappings", 1.67 + mapping.get().method(), 1.68 + mapping.get().requestPath() 1.69 + ); 1.70 + } 1.71 + 1.72 + LOG.debug("{} {} maps to {}::{}", 1.73 mapping.get().method(), 1.74 - mapping.get().requestPath() 1.75 + mapping.get().requestPath(), 1.76 + getClass().getSimpleName(), 1.77 + method.getName() 1.78 + ); 1.79 + } catch (IllegalArgumentException ex) { 1.80 + LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid", 1.81 + method.getName(), mapping.get().requestPath() 1.82 ); 1.83 } 1.84 - 1.85 - LOG.debug("{} {} maps to {}::{}", 1.86 - mapping.get().method(), 1.87 - requestPath, 1.88 - getClass().getSimpleName(), 1.89 - method.getName() 1.90 - ); 1.91 } else { 1.92 - LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest. HttpServletResponse, and DataAccessObjects are allowed", 1.93 + LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed", 1.94 method.getName(), RequestMapping.class.getSimpleName() 1.95 ); 1.96 } 1.97 @@ -373,8 +392,12 @@ 1.98 return Optional.ofNullable(req.getPathInfo()).orElse("/"); 1.99 } 1.100 1.101 - private Optional<Method> findMapping(HttpMethod method, HttpServletRequest req) { 1.102 - return Optional.ofNullable(mappings.get(method)).map(rm -> rm.get(sanitizeRequestPath(req))); 1.103 + private Optional<Map.Entry<PathPattern, Method>> findMapping(HttpMethod method, HttpServletRequest req) { 1.104 + return Optional.ofNullable(mappings.get(method)).flatMap(rm -> 1.105 + rm.entrySet().stream().filter( 1.106 + kv -> kv.getKey().matches(sanitizeRequestPath(req)) 1.107 + ).findAny() 1.108 + ); 1.109 } 1.110 1.111 private void forwardAsSpecified(ResponseType type, HttpServletRequest req, HttpServletResponse resp)
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/src/main/java/de/uapcore/lightpit/PathParameters.java Thu Oct 15 18:36:05 2020 +0200 2.3 @@ -0,0 +1,6 @@ 2.4 +package de.uapcore.lightpit; 2.5 + 2.6 +import java.util.HashMap; 2.7 + 2.8 +public class PathParameters extends HashMap<String, String> { 2.9 +}
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/src/main/java/de/uapcore/lightpit/PathPattern.java Thu Oct 15 18:36:05 2020 +0200 3.3 @@ -0,0 +1,125 @@ 3.4 +package de.uapcore.lightpit; 3.5 + 3.6 +import java.util.ArrayList; 3.7 +import java.util.List; 3.8 + 3.9 +public final class PathPattern { 3.10 + 3.11 + private final List<String> nodePatterns; 3.12 + private final boolean collection; 3.13 + 3.14 + /** 3.15 + * Constructs a new path pattern. 3.16 + * The special directories . and .. are disallowed in the pattern. 3.17 + * 3.18 + * @param pattern 3.19 + */ 3.20 + public PathPattern(String pattern) { 3.21 + nodePatterns = parse(pattern); 3.22 + collection = pattern.endsWith("/"); 3.23 + } 3.24 + 3.25 + private List<String> parse(String pattern) { 3.26 + 3.27 + var nodes = new ArrayList<String>(); 3.28 + var parts = pattern.split("/"); 3.29 + 3.30 + for (var part : parts) { 3.31 + if (part.isBlank()) continue; 3.32 + if (part.equals(".") || part.equals("..")) 3.33 + throw new IllegalArgumentException("Path must not contain '.' or '..' nodes."); 3.34 + nodes.add(part); 3.35 + } 3.36 + 3.37 + return nodes; 3.38 + } 3.39 + 3.40 + /** 3.41 + * Matches a path against this pattern. 3.42 + * The path must be canonical in the sense that no . or .. parts occur. 3.43 + * 3.44 + * @param path the path to match 3.45 + * @return true if the path matches the pattern, false otherwise 3.46 + */ 3.47 + public boolean matches(String path) { 3.48 + if (collection ^ path.endsWith("/")) 3.49 + return false; 3.50 + 3.51 + var nodes = parse(path); 3.52 + if (nodePatterns.size() != nodes.size()) 3.53 + return false; 3.54 + 3.55 + for (int i = 0 ; i < nodePatterns.size() ; i++) { 3.56 + var pattern = nodePatterns.get(i); 3.57 + var node = nodes.get(i); 3.58 + if (pattern.startsWith("$")) 3.59 + continue; 3.60 + if (!pattern.equals(node)) 3.61 + return false; 3.62 + } 3.63 + 3.64 + return true; 3.65 + } 3.66 + 3.67 + /** 3.68 + * Returns the path parameters found in the specified path using this pattern. 3.69 + * The return value of this method is undefined, if the patter does not match. 3.70 + * 3.71 + * @param path the path 3.72 + * @return the path parameters, if any, or an empty map 3.73 + * @see #matches(String) 3.74 + */ 3.75 + public PathParameters obtainPathParameters(String path) { 3.76 + var params = new PathParameters(); 3.77 + 3.78 + var nodes = parse(path); 3.79 + 3.80 + for (int i = 0 ; i < Math.min(nodes.size(), nodePatterns.size()) ; i++) { 3.81 + var pattern = nodePatterns.get(i); 3.82 + var node = nodes.get(i); 3.83 + if (pattern.startsWith("$")) { 3.84 + params.put(pattern.substring(1), node); 3.85 + } 3.86 + } 3.87 + 3.88 + return params; 3.89 + } 3.90 + 3.91 + @Override 3.92 + public int hashCode() { 3.93 + var str = new StringBuilder(); 3.94 + for (var node : nodePatterns) { 3.95 + if (node.startsWith("$")) { 3.96 + str.append("/$"); 3.97 + } else { 3.98 + str.append('/'); 3.99 + str.append(node); 3.100 + } 3.101 + } 3.102 + if (collection) 3.103 + str.append('/'); 3.104 + 3.105 + return str.toString().hashCode(); 3.106 + } 3.107 + 3.108 + @Override 3.109 + public boolean equals(Object obj) { 3.110 + if (!obj.getClass().equals(PathPattern.class)) 3.111 + return false; 3.112 + 3.113 + var other = (PathPattern) obj; 3.114 + if (collection ^ other.collection || nodePatterns.size() != other.nodePatterns.size()) 3.115 + return false; 3.116 + 3.117 + for (int i = 0 ; i < nodePatterns.size() ; i++) { 3.118 + var left = nodePatterns.get(i); 3.119 + var right = other.nodePatterns.get(i); 3.120 + if (left.startsWith("$") && right.startsWith("$")) 3.121 + continue; 3.122 + if (!left.equals(right)) 3.123 + return false; 3.124 + } 3.125 + 3.126 + return true; 3.127 + } 3.128 +}
4.1 --- a/src/main/java/de/uapcore/lightpit/RequestMapping.java Thu Oct 15 14:01:49 2020 +0200 4.2 +++ b/src/main/java/de/uapcore/lightpit/RequestMapping.java Thu Oct 15 18:36:05 2020 +0200 4.3 @@ -51,10 +51,12 @@ 4.4 4.5 /** 4.6 * Specifies the request path relative to the module path. 4.7 - * The path must be specified <em>without</em> leading slash, but may have a trailing slash. 4.8 - * Requests will only be handled if the path exactly matches. 4.9 + * The trailing slash is important. 4.10 + * A node may start with a dollar ($) sign. 4.11 + * This part of the path is then treated as an path parameter. 4.12 + * Path parameters can be obtained by including the {@link PathParameters} interface in the signature. 4.13 * 4.14 * @return the request path the annotated method should handle 4.15 */ 4.16 - String requestPath() default ""; 4.17 + String requestPath() default "/"; 4.18 }