adds the possibility to specify path parameters to RequestMapping

Thu, 15 Oct 2020 18:36:05 +0200

author
Mike Becker <universe@uap-core.de>
date
Thu, 15 Oct 2020 18:36:05 +0200
changeset 130
7ef369744fd1
parent 129
a09d5c59351a
child 131
67df332e3146

adds the possibility to specify path parameters to RequestMapping

src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/PathParameters.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/PathPattern.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/RequestMapping.java file | annotate | diff | comparison | revisions
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Thu Oct 15 14:01:49 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Thu Oct 15 18:36:05 2020 +0200
@@ -84,7 +84,7 @@
      * The reason for this is the different handling of empty paths in
      * {@link HttpServletRequest#getPathInfo()}.
      */
-    private final Map<HttpMethod, Map<String, Method>> mappings = new HashMap<>();
+    private final Map<HttpMethod, Map<PathPattern, Method>> mappings = new HashMap<>();
 
     /**
      * Returns the name of the resource bundle associated with this servlet.
@@ -108,7 +108,9 @@
         throw new AssertionError("Non-exhaustive if-else - this is a bug.");
     }
 
-    private ResponseType invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
+    private ResponseType invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
+        final var pathPattern = mapping.getKey();
+        final var method = mapping.getValue();
         try {
             LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName());
             final var paramTypes = method.getParameterTypes();
@@ -122,6 +124,9 @@
                 if (paramTypes[i].isAssignableFrom(DataAccessObjects.class)) {
                     paramValues[i] = dao;
                 }
+                if (paramTypes[i].isAssignableFrom(PathParameters.class)) {
+                    paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req));
+                }
             }
             return (ResponseType) method.invoke(this, paramValues);
         } catch (InvocationTargetException ex) {
@@ -152,6 +157,13 @@
             for (Method method : methods) {
                 Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
                 if (mapping.isPresent()) {
+                    if (mapping.get().requestPath().isBlank()) {
+                        LOG.warn("{} is annotated with {} but request path is empty",
+                                method.getName(), RequestMapping.class.getSimpleName()
+                        );
+                        continue;
+                    }
+
                     if (!Modifier.isPublic(method.getModifiers())) {
                         LOG.warn("{} is annotated with {} but is not public",
                                 method.getName(), RequestMapping.class.getSimpleName()
@@ -175,28 +187,35 @@
                     for (var param : method.getParameterTypes()) {
                         paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param)
                                 || HttpServletResponse.class.isAssignableFrom(param)
+                                || PathParameters.class.isAssignableFrom(param)
                                 || DataAccessObjects.class.isAssignableFrom(param);
                     }
                     if (paramsInjectible) {
-                        String requestPath = "/" + mapping.get().requestPath();
+                        try {
+                            PathPattern pathPattern = new PathPattern(mapping.get().requestPath());
 
-                        if (mappings
-                                .computeIfAbsent(mapping.get().method(), k -> new HashMap<>())
-                                .putIfAbsent(requestPath, method) != null) {
-                            LOG.warn("{} {} has multiple mappings",
+                            if (mappings
+                                    .computeIfAbsent(mapping.get().method(), k -> new HashMap<>())
+                                    .putIfAbsent(pathPattern, method) != null) {
+                                LOG.warn("{} {} has multiple mappings",
+                                        mapping.get().method(),
+                                        mapping.get().requestPath()
+                                );
+                            }
+
+                            LOG.debug("{} {} maps to {}::{}",
                                     mapping.get().method(),
-                                    mapping.get().requestPath()
+                                    mapping.get().requestPath(),
+                                    getClass().getSimpleName(),
+                                    method.getName()
+                            );
+                        } catch (IllegalArgumentException ex) {
+                            LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid",
+                                    method.getName(), mapping.get().requestPath()
                             );
                         }
-
-                        LOG.debug("{} {} maps to {}::{}",
-                                mapping.get().method(),
-                                requestPath,
-                                getClass().getSimpleName(),
-                                method.getName()
-                        );
                     } else {
-                        LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest. HttpServletResponse, and DataAccessObjects are allowed",
+                        LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed",
                                 method.getName(), RequestMapping.class.getSimpleName()
                         );
                     }
@@ -373,8 +392,12 @@
         return Optional.ofNullable(req.getPathInfo()).orElse("/");
     }
 
-    private Optional<Method> findMapping(HttpMethod method, HttpServletRequest req) {
-        return Optional.ofNullable(mappings.get(method)).map(rm -> rm.get(sanitizeRequestPath(req)));
+    private Optional<Map.Entry<PathPattern, Method>> findMapping(HttpMethod method, HttpServletRequest req) {
+        return Optional.ofNullable(mappings.get(method)).flatMap(rm ->
+                rm.entrySet().stream().filter(
+                        kv -> kv.getKey().matches(sanitizeRequestPath(req))
+                ).findAny()
+        );
     }
 
     private void forwardAsSpecified(ResponseType type, HttpServletRequest req, HttpServletResponse resp)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/PathParameters.java	Thu Oct 15 18:36:05 2020 +0200
@@ -0,0 +1,6 @@
+package de.uapcore.lightpit;
+
+import java.util.HashMap;
+
+public class PathParameters extends HashMap<String, String> {
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/PathPattern.java	Thu Oct 15 18:36:05 2020 +0200
@@ -0,0 +1,125 @@
+package de.uapcore.lightpit;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class PathPattern {
+
+    private final List<String> nodePatterns;
+    private final boolean collection;
+
+    /**
+     * Constructs a new path pattern.
+     * The special directories . and .. are disallowed in the pattern.
+     *
+     * @param pattern
+     */
+    public PathPattern(String pattern) {
+        nodePatterns = parse(pattern);
+        collection = pattern.endsWith("/");
+    }
+
+    private List<String> parse(String pattern) {
+
+        var nodes = new ArrayList<String>();
+        var parts = pattern.split("/");
+
+        for (var part : parts) {
+            if (part.isBlank()) continue;
+            if (part.equals(".") || part.equals(".."))
+                throw new IllegalArgumentException("Path must not contain '.' or '..' nodes.");
+            nodes.add(part);
+        }
+
+        return nodes;
+    }
+
+    /**
+     * Matches a path against this pattern.
+     * The path must be canonical in the sense that no . or .. parts occur.
+     *
+     * @param path the path to match
+     * @return true if the path matches the pattern, false otherwise
+     */
+    public boolean matches(String path) {
+        if (collection ^ path.endsWith("/"))
+            return false;
+
+        var nodes = parse(path);
+        if (nodePatterns.size() != nodes.size())
+            return false;
+
+        for (int i = 0 ; i < nodePatterns.size() ; i++) {
+            var pattern = nodePatterns.get(i);
+            var node = nodes.get(i);
+            if (pattern.startsWith("$"))
+                continue;
+            if (!pattern.equals(node))
+                return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the path parameters found in the specified path using this pattern.
+     * The return value of this method is undefined, if the patter does not match.
+     *
+     * @param path the path
+     * @return the path parameters, if any, or an empty map
+     * @see #matches(String)
+     */
+    public PathParameters obtainPathParameters(String path) {
+        var params = new PathParameters();
+
+        var nodes = parse(path);
+
+        for (int i = 0 ; i < Math.min(nodes.size(), nodePatterns.size()) ; i++) {
+            var pattern = nodePatterns.get(i);
+            var node = nodes.get(i);
+            if (pattern.startsWith("$")) {
+                params.put(pattern.substring(1), node);
+            }
+        }
+
+        return params;
+    }
+
+    @Override
+    public int hashCode() {
+        var str = new StringBuilder();
+        for (var node : nodePatterns) {
+            if (node.startsWith("$")) {
+                str.append("/$");
+            } else {
+                str.append('/');
+                str.append(node);
+            }
+        }
+        if (collection)
+            str.append('/');
+
+        return str.toString().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!obj.getClass().equals(PathPattern.class))
+            return false;
+
+        var other = (PathPattern) obj;
+        if (collection ^ other.collection || nodePatterns.size() != other.nodePatterns.size())
+            return false;
+
+        for (int i = 0 ; i < nodePatterns.size() ; i++) {
+            var left = nodePatterns.get(i);
+            var right = other.nodePatterns.get(i);
+            if (left.startsWith("$") && right.startsWith("$"))
+                continue;
+            if (!left.equals(right))
+                return false;
+        }
+
+        return true;
+    }
+}
--- a/src/main/java/de/uapcore/lightpit/RequestMapping.java	Thu Oct 15 14:01:49 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/RequestMapping.java	Thu Oct 15 18:36:05 2020 +0200
@@ -51,10 +51,12 @@
 
     /**
      * Specifies the request path relative to the module path.
-     * The path must be specified <em>without</em> leading slash, but may have a trailing slash.
-     * Requests will only be handled if the path exactly matches.
+     * The trailing slash is important.
+     * A node may start with a dollar ($) sign.
+     * This part of the path is then treated as an path parameter.
+     * Path parameters can be obtained by including the {@link PathParameters} interface in the signature.
      *
      * @return the request path the annotated method should handle
      */
-    String requestPath() default "";
+    String requestPath() default "/";
 }

mercurial