82 * the specification in the annotation must not start with a leading slash. |
82 * the specification in the annotation must not start with a leading slash. |
83 * <p> |
83 * <p> |
84 * The reason for this is the different handling of empty paths in |
84 * The reason for this is the different handling of empty paths in |
85 * {@link HttpServletRequest#getPathInfo()}. |
85 * {@link HttpServletRequest#getPathInfo()}. |
86 */ |
86 */ |
87 private final Map<HttpMethod, Map<String, Method>> mappings = new HashMap<>(); |
87 private final Map<HttpMethod, Map<PathPattern, Method>> mappings = new HashMap<>(); |
88 |
88 |
89 /** |
89 /** |
90 * Returns the name of the resource bundle associated with this servlet. |
90 * Returns the name of the resource bundle associated with this servlet. |
91 * |
91 * |
92 * @return the resource bundle base name |
92 * @return the resource bundle base name |
106 return new PGDataAccessObjects(connection); |
106 return new PGDataAccessObjects(connection); |
107 } |
107 } |
108 throw new AssertionError("Non-exhaustive if-else - this is a bug."); |
108 throw new AssertionError("Non-exhaustive if-else - this is a bug."); |
109 } |
109 } |
110 |
110 |
111 private ResponseType invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException { |
111 private ResponseType invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException { |
|
112 final var pathPattern = mapping.getKey(); |
|
113 final var method = mapping.getValue(); |
112 try { |
114 try { |
113 LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName()); |
115 LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName()); |
114 final var paramTypes = method.getParameterTypes(); |
116 final var paramTypes = method.getParameterTypes(); |
115 final var paramValues = new Object[paramTypes.length]; |
117 final var paramValues = new Object[paramTypes.length]; |
116 for (int i = 0; i < paramTypes.length; i++) { |
118 for (int i = 0; i < paramTypes.length; i++) { |
119 } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) { |
121 } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) { |
120 paramValues[i] = resp; |
122 paramValues[i] = resp; |
121 } |
123 } |
122 if (paramTypes[i].isAssignableFrom(DataAccessObjects.class)) { |
124 if (paramTypes[i].isAssignableFrom(DataAccessObjects.class)) { |
123 paramValues[i] = dao; |
125 paramValues[i] = dao; |
|
126 } |
|
127 if (paramTypes[i].isAssignableFrom(PathParameters.class)) { |
|
128 paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req)); |
124 } |
129 } |
125 } |
130 } |
126 return (ResponseType) method.invoke(this, paramValues); |
131 return (ResponseType) method.invoke(this, paramValues); |
127 } catch (InvocationTargetException ex) { |
132 } catch (InvocationTargetException ex) { |
128 LOG.error("invocation of method {}::{} failed: {}", |
133 LOG.error("invocation of method {}::{} failed: {}", |
150 try { |
155 try { |
151 Method[] methods = getClass().getDeclaredMethods(); |
156 Method[] methods = getClass().getDeclaredMethods(); |
152 for (Method method : methods) { |
157 for (Method method : methods) { |
153 Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class)); |
158 Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class)); |
154 if (mapping.isPresent()) { |
159 if (mapping.isPresent()) { |
|
160 if (mapping.get().requestPath().isBlank()) { |
|
161 LOG.warn("{} is annotated with {} but request path is empty", |
|
162 method.getName(), RequestMapping.class.getSimpleName() |
|
163 ); |
|
164 continue; |
|
165 } |
|
166 |
155 if (!Modifier.isPublic(method.getModifiers())) { |
167 if (!Modifier.isPublic(method.getModifiers())) { |
156 LOG.warn("{} is annotated with {} but is not public", |
168 LOG.warn("{} is annotated with {} but is not public", |
157 method.getName(), RequestMapping.class.getSimpleName() |
169 method.getName(), RequestMapping.class.getSimpleName() |
158 ); |
170 ); |
159 continue; |
171 continue; |
173 |
185 |
174 boolean paramsInjectible = true; |
186 boolean paramsInjectible = true; |
175 for (var param : method.getParameterTypes()) { |
187 for (var param : method.getParameterTypes()) { |
176 paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param) |
188 paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param) |
177 || HttpServletResponse.class.isAssignableFrom(param) |
189 || HttpServletResponse.class.isAssignableFrom(param) |
|
190 || PathParameters.class.isAssignableFrom(param) |
178 || DataAccessObjects.class.isAssignableFrom(param); |
191 || DataAccessObjects.class.isAssignableFrom(param); |
179 } |
192 } |
180 if (paramsInjectible) { |
193 if (paramsInjectible) { |
181 String requestPath = "/" + mapping.get().requestPath(); |
194 try { |
182 |
195 PathPattern pathPattern = new PathPattern(mapping.get().requestPath()); |
183 if (mappings |
196 |
184 .computeIfAbsent(mapping.get().method(), k -> new HashMap<>()) |
197 if (mappings |
185 .putIfAbsent(requestPath, method) != null) { |
198 .computeIfAbsent(mapping.get().method(), k -> new HashMap<>()) |
186 LOG.warn("{} {} has multiple mappings", |
199 .putIfAbsent(pathPattern, method) != null) { |
|
200 LOG.warn("{} {} has multiple mappings", |
|
201 mapping.get().method(), |
|
202 mapping.get().requestPath() |
|
203 ); |
|
204 } |
|
205 |
|
206 LOG.debug("{} {} maps to {}::{}", |
187 mapping.get().method(), |
207 mapping.get().method(), |
188 mapping.get().requestPath() |
208 mapping.get().requestPath(), |
|
209 getClass().getSimpleName(), |
|
210 method.getName() |
|
211 ); |
|
212 } catch (IllegalArgumentException ex) { |
|
213 LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid", |
|
214 method.getName(), mapping.get().requestPath() |
189 ); |
215 ); |
190 } |
216 } |
191 |
|
192 LOG.debug("{} {} maps to {}::{}", |
|
193 mapping.get().method(), |
|
194 requestPath, |
|
195 getClass().getSimpleName(), |
|
196 method.getName() |
|
197 ); |
|
198 } else { |
217 } else { |
199 LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest. HttpServletResponse, and DataAccessObjects are allowed", |
218 LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed", |
200 method.getName(), RequestMapping.class.getSimpleName() |
219 method.getName(), RequestMapping.class.getSimpleName() |
201 ); |
220 ); |
202 } |
221 } |
203 } |
222 } |
204 } |
223 } |
371 |
390 |
372 private String sanitizeRequestPath(HttpServletRequest req) { |
391 private String sanitizeRequestPath(HttpServletRequest req) { |
373 return Optional.ofNullable(req.getPathInfo()).orElse("/"); |
392 return Optional.ofNullable(req.getPathInfo()).orElse("/"); |
374 } |
393 } |
375 |
394 |
376 private Optional<Method> findMapping(HttpMethod method, HttpServletRequest req) { |
395 private Optional<Map.Entry<PathPattern, Method>> findMapping(HttpMethod method, HttpServletRequest req) { |
377 return Optional.ofNullable(mappings.get(method)).map(rm -> rm.get(sanitizeRequestPath(req))); |
396 return Optional.ofNullable(mappings.get(method)).flatMap(rm -> |
|
397 rm.entrySet().stream().filter( |
|
398 kv -> kv.getKey().matches(sanitizeRequestPath(req)) |
|
399 ).findAny() |
|
400 ); |
378 } |
401 } |
379 |
402 |
380 private void forwardAsSpecified(ResponseType type, HttpServletRequest req, HttpServletResponse resp) |
403 private void forwardAsSpecified(ResponseType type, HttpServletRequest req, HttpServletResponse resp) |
381 throws ServletException, IOException { |
404 throws ServletException, IOException { |
382 switch (type) { |
405 switch (type) { |