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
     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  }

mercurial