Tue, 05 Jan 2021 19:19:31 +0100
migrates the utility classes for the AbstractServlet
1.1 --- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Mon Jan 04 17:30:10 2021 +0100 1.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 1.3 @@ -1,471 +0,0 @@ 1.4 -/* 1.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 1.6 - * 1.7 - * Copyright 2018 Mike Becker. All rights reserved. 1.8 - * 1.9 - * Redistribution and use in source and binary forms, with or without 1.10 - * modification, are permitted provided that the following conditions are met: 1.11 - * 1.12 - * 1. Redistributions of source code must retain the above copyright 1.13 - * notice, this list of conditions and the following disclaimer. 1.14 - * 1.15 - * 2. Redistributions in binary form must reproduce the above copyright 1.16 - * notice, this list of conditions and the following disclaimer in the 1.17 - * documentation and/or other materials provided with the distribution. 1.18 - * 1.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 1.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 1.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 1.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 1.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 1.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 1.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 1.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 1.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 1.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 1.29 - * POSSIBILITY OF SUCH DAMAGE. 1.30 - * 1.31 - */ 1.32 -package de.uapcore.lightpit; 1.33 - 1.34 -import de.uapcore.lightpit.dao.DataAccessObject; 1.35 -import de.uapcore.lightpit.dao.PostgresDataAccessObject; 1.36 -import org.slf4j.Logger; 1.37 -import org.slf4j.LoggerFactory; 1.38 - 1.39 -import javax.servlet.ServletException; 1.40 -import javax.servlet.http.HttpServlet; 1.41 -import javax.servlet.http.HttpServletRequest; 1.42 -import javax.servlet.http.HttpServletResponse; 1.43 -import javax.servlet.http.HttpSession; 1.44 -import java.io.IOException; 1.45 -import java.lang.reflect.*; 1.46 -import java.sql.Connection; 1.47 -import java.sql.SQLException; 1.48 -import java.util.*; 1.49 -import java.util.function.Function; 1.50 -import java.util.stream.Collectors; 1.51 - 1.52 -/** 1.53 - * A special implementation of a HTTPServlet which is focused on implementing 1.54 - * the necessary functionality for LightPIT pages. 1.55 - */ 1.56 -public abstract class AbstractLightPITServlet extends HttpServlet { 1.57 - 1.58 - private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class); 1.59 - 1.60 - private static final String SITE_JSP = jspPath("site"); 1.61 - 1.62 - /** 1.63 - * Invocation mapping gathered from the {@link RequestMapping} annotations. 1.64 - * <p> 1.65 - * Paths in this map must always start with a leading slash, although 1.66 - * the specification in the annotation must not start with a leading slash. 1.67 - * <p> 1.68 - * The reason for this is the different handling of empty paths in 1.69 - * {@link HttpServletRequest#getPathInfo()}. 1.70 - */ 1.71 - private final Map<HttpMethod, Map<PathPattern, Method>> mappings = new HashMap<>(); 1.72 - 1.73 - /** 1.74 - * Returns the name of the resource bundle associated with this servlet. 1.75 - * 1.76 - * @return the resource bundle base name 1.77 - */ 1.78 - protected abstract String getResourceBundleName(); 1.79 - 1.80 - 1.81 - /** 1.82 - * Creates a set of data access objects for the specified connection. 1.83 - * 1.84 - * @param connection the SQL connection 1.85 - * @return a set of data access objects 1.86 - */ 1.87 - private DataAccessObject createDataAccessObjects(Connection connection) { 1.88 - final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME()); 1.89 - if (df.getDialect() == DataSourceProvider.Dialect.Postgres) { 1.90 - return new PostgresDataAccessObject(connection); 1.91 - } 1.92 - throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug."); 1.93 - } 1.94 - 1.95 - private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException { 1.96 - final var pathPattern = mapping.getKey(); 1.97 - final var method = mapping.getValue(); 1.98 - try { 1.99 - LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName()); 1.100 - final var paramTypes = method.getParameterTypes(); 1.101 - final var paramValues = new Object[paramTypes.length]; 1.102 - for (int i = 0; i < paramTypes.length; i++) { 1.103 - if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) { 1.104 - paramValues[i] = req; 1.105 - } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) { 1.106 - paramValues[i] = resp; 1.107 - } 1.108 - if (paramTypes[i].isAssignableFrom(DataAccessObject.class)) { 1.109 - paramValues[i] = dao; 1.110 - } 1.111 - if (paramTypes[i].isAssignableFrom(PathParameters.class)) { 1.112 - paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req)); 1.113 - } 1.114 - } 1.115 - method.invoke(this, paramValues); 1.116 - } catch (InvocationTargetException ex) { 1.117 - LOG.error("invocation of method {}::{} failed: {}", 1.118 - method.getDeclaringClass().getName(), method.getName(), ex.getTargetException().getMessage()); 1.119 - LOG.debug("Details: ", ex.getTargetException()); 1.120 - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getTargetException().getMessage()); 1.121 - } catch (ReflectiveOperationException | ClassCastException ex) { 1.122 - LOG.error("invocation of method {}::{} failed: {}", 1.123 - method.getDeclaringClass().getName(), method.getName(), ex.getMessage()); 1.124 - LOG.debug("Details: ", ex); 1.125 - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); 1.126 - } 1.127 - } 1.128 - 1.129 - @Override 1.130 - public void init() throws ServletException { 1.131 - scanForRequestMappings(); 1.132 - 1.133 - LOG.trace("{} initialized", getServletName()); 1.134 - } 1.135 - 1.136 - private void scanForRequestMappings() { 1.137 - try { 1.138 - Method[] methods = getClass().getDeclaredMethods(); 1.139 - for (Method method : methods) { 1.140 - Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class)); 1.141 - if (mapping.isPresent()) { 1.142 - if (mapping.get().requestPath().isBlank()) { 1.143 - LOG.warn("{} is annotated with {} but request path is empty", 1.144 - method.getName(), RequestMapping.class.getSimpleName() 1.145 - ); 1.146 - continue; 1.147 - } 1.148 - 1.149 - if (!Modifier.isPublic(method.getModifiers())) { 1.150 - LOG.warn("{} is annotated with {} but is not public", 1.151 - method.getName(), RequestMapping.class.getSimpleName() 1.152 - ); 1.153 - continue; 1.154 - } 1.155 - if (Modifier.isAbstract(method.getModifiers())) { 1.156 - LOG.warn("{} is annotated with {} but is abstract", 1.157 - method.getName(), RequestMapping.class.getSimpleName() 1.158 - ); 1.159 - continue; 1.160 - } 1.161 - 1.162 - boolean paramsInjectible = true; 1.163 - for (var param : method.getParameterTypes()) { 1.164 - paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param) 1.165 - || HttpServletResponse.class.isAssignableFrom(param) 1.166 - || PathParameters.class.isAssignableFrom(param) 1.167 - || DataAccessObject.class.isAssignableFrom(param); 1.168 - } 1.169 - if (paramsInjectible) { 1.170 - try { 1.171 - PathPattern pathPattern = new PathPattern(mapping.get().requestPath()); 1.172 - 1.173 - final var methodMappings = mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>()); 1.174 - final var currentMapping = methodMappings.putIfAbsent(pathPattern, method); 1.175 - if (currentMapping != null) { 1.176 - LOG.warn("Cannot map {} {} to {} in class {} - this would override the mapping to {}", 1.177 - mapping.get().method(), 1.178 - mapping.get().requestPath(), 1.179 - method.getName(), 1.180 - getClass().getSimpleName(), 1.181 - currentMapping.getName() 1.182 - ); 1.183 - } 1.184 - 1.185 - LOG.debug("{} {} maps to {}::{}", 1.186 - mapping.get().method(), 1.187 - mapping.get().requestPath(), 1.188 - getClass().getSimpleName(), 1.189 - method.getName() 1.190 - ); 1.191 - } catch (IllegalArgumentException ex) { 1.192 - LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid", 1.193 - method.getName(), mapping.get().requestPath() 1.194 - ); 1.195 - } 1.196 - } else { 1.197 - LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed", 1.198 - method.getName(), RequestMapping.class.getSimpleName() 1.199 - ); 1.200 - } 1.201 - } 1.202 - } 1.203 - } catch (SecurityException ex) { 1.204 - LOG.error("Scan for request mappings on declared methods failed.", ex); 1.205 - } 1.206 - } 1.207 - 1.208 - @Override 1.209 - public void destroy() { 1.210 - mappings.clear(); 1.211 - LOG.trace("{} destroyed", getServletName()); 1.212 - } 1.213 - 1.214 - /** 1.215 - * Sets the name of the content page. 1.216 - * <p> 1.217 - * It is sufficient to specify the name without any extension. The extension 1.218 - * is added automatically if not specified. 1.219 - * 1.220 - * @param req the servlet request object 1.221 - * @param pageName the name of the content page 1.222 - * @see Constants#REQ_ATTR_CONTENT_PAGE 1.223 - */ 1.224 - protected void setContentPage(HttpServletRequest req, String pageName) { 1.225 - req.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(pageName)); 1.226 - } 1.227 - 1.228 - /** 1.229 - * Sets the navigation menu. 1.230 - * 1.231 - * @param req the servlet request object 1.232 - * @param jspName the name of the menu's jsp file 1.233 - * @see Constants#REQ_ATTR_NAVIGATION 1.234 - */ 1.235 - protected void setNavigationMenu(HttpServletRequest req, String jspName) { 1.236 - req.setAttribute(Constants.REQ_ATTR_NAVIGATION, jspPath(jspName)); 1.237 - } 1.238 - 1.239 - /** 1.240 - * @param req the servlet request object 1.241 - * @param location the location where to redirect 1.242 - * @see Constants#REQ_ATTR_REDIRECT_LOCATION 1.243 - */ 1.244 - protected void setRedirectLocation(HttpServletRequest req, String location) { 1.245 - if (location.startsWith("./")) { 1.246 - location = location.replaceFirst("\\./", baseHref(req)); 1.247 - } 1.248 - req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location); 1.249 - } 1.250 - 1.251 - /** 1.252 - * Specifies the names of additional stylesheets used by this Servlet. 1.253 - * <p> 1.254 - * It is sufficient to specify the name without any extension. The extension 1.255 - * is added automatically if not specified. 1.256 - * 1.257 - * @param req the servlet request object 1.258 - * @param stylesheets the names of the stylesheets 1.259 - */ 1.260 - public void setStylesheet(HttpServletRequest req, String ... stylesheets) { 1.261 - req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Arrays 1.262 - .stream(stylesheets) 1.263 - .map(s -> enforceExt(s, ".css")) 1.264 - .collect(Collectors.toUnmodifiableList())); 1.265 - } 1.266 - 1.267 - /** 1.268 - * Sets the view model object. 1.269 - * The type must match the expected type in the JSP file. 1.270 - * 1.271 - * @param req the servlet request object 1.272 - * @param viewModel the view model object 1.273 - */ 1.274 - public void setViewModel(HttpServletRequest req, Object viewModel) { 1.275 - req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel); 1.276 - } 1.277 - 1.278 - private <T> Optional<T> parseParameter(String paramValue, Class<T> clazz) { 1.279 - if (paramValue == null) return Optional.empty(); 1.280 - if (clazz.equals(Boolean.class)) { 1.281 - if (paramValue.equalsIgnoreCase("false") || paramValue.equals("0")) { 1.282 - return Optional.of((T) Boolean.FALSE); 1.283 - } else { 1.284 - return Optional.of((T) Boolean.TRUE); 1.285 - } 1.286 - } 1.287 - if (clazz.equals(String.class)) return Optional.of((T) paramValue); 1.288 - if (java.sql.Date.class.isAssignableFrom(clazz)) { 1.289 - try { 1.290 - return Optional.of((T) java.sql.Date.valueOf(paramValue)); 1.291 - } catch (IllegalArgumentException ex) { 1.292 - return Optional.empty(); 1.293 - } 1.294 - } 1.295 - try { 1.296 - final Constructor<T> ctor = clazz.getConstructor(String.class); 1.297 - return Optional.of(ctor.newInstance(paramValue)); 1.298 - } catch (ReflectiveOperationException e) { 1.299 - // does not type check and is not convertible - treat as if the parameter was never set 1.300 - return Optional.empty(); 1.301 - } 1.302 - } 1.303 - 1.304 - /** 1.305 - * Obtains a request parameter of the specified type. 1.306 - * The specified type must have a single-argument constructor accepting a string to perform conversion. 1.307 - * The constructor of the specified type may throw an exception on conversion failures. 1.308 - * 1.309 - * @param req the servlet request object 1.310 - * @param clazz the class object of the expected type 1.311 - * @param name the name of the parameter 1.312 - * @param <T> the expected type 1.313 - * @return the parameter value or an empty optional, if no parameter with the specified name was found 1.314 - */ 1.315 - protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) { 1.316 - if (clazz.isArray()) { 1.317 - final String[] paramValues = req.getParameterValues(name); 1.318 - int len = paramValues == null ? 0 : paramValues.length; 1.319 - final var array = (T) Array.newInstance(clazz.getComponentType(), len); 1.320 - for (int i = 0; i < len; i++) { 1.321 - try { 1.322 - final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class); 1.323 - Array.set(array, i, ctor.newInstance(paramValues[i])); 1.324 - } catch (ReflectiveOperationException e) { 1.325 - throw new RuntimeException(e); 1.326 - } 1.327 - } 1.328 - return Optional.of(array); 1.329 - } else { 1.330 - return parseParameter(req.getParameter(name), clazz); 1.331 - } 1.332 - } 1.333 - 1.334 - /** 1.335 - * Tries to look up an entity with a key obtained from a request parameter. 1.336 - * 1.337 - * @param req the servlet request object 1.338 - * @param clazz the class representing the type of the request parameter 1.339 - * @param name the name of the request parameter 1.340 - * @param find the find function (typically a DAO function) 1.341 - * @param <T> the type of the request parameter 1.342 - * @param <R> the type of the looked up entity 1.343 - * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing 1.344 - * @throws SQLException if the find function throws an exception 1.345 - */ 1.346 - protected <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, Function<? super T, ? extends R> find) { 1.347 - final var param = getParameter(req, clazz, name); 1.348 - if (param.isPresent()) { 1.349 - return Optional.ofNullable(find.apply(param.get())); 1.350 - } else { 1.351 - return Optional.empty(); 1.352 - } 1.353 - } 1.354 - 1.355 - protected void setAttributeFromParameter(HttpServletRequest req, String name) { 1.356 - final var parm = req.getParameter(name); 1.357 - if (parm != null) { 1.358 - req.setAttribute(name, parm); 1.359 - } 1.360 - } 1.361 - 1.362 - private String sanitizeRequestPath(HttpServletRequest req) { 1.363 - return Optional.ofNullable(req.getPathInfo()).orElse("/"); 1.364 - } 1.365 - 1.366 - private Optional<Map.Entry<PathPattern, Method>> findMapping(HttpMethod method, HttpServletRequest req) { 1.367 - return Optional.ofNullable(mappings.get(method)).flatMap(rm -> 1.368 - rm.entrySet().stream().filter( 1.369 - kv -> kv.getKey().matches(sanitizeRequestPath(req)) 1.370 - ).findAny() 1.371 - ); 1.372 - } 1.373 - 1.374 - protected void renderSite(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 1.375 - req.getRequestDispatcher(SITE_JSP).forward(req, resp); 1.376 - } 1.377 - 1.378 - protected Optional<String[]> availableLanguages() { 1.379 - return Optional.ofNullable(getServletContext().getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*")); 1.380 - } 1.381 - 1.382 - private static String baseHref(HttpServletRequest req) { 1.383 - return String.format("%s://%s:%d%s/", 1.384 - req.getScheme(), 1.385 - req.getServerName(), 1.386 - req.getServerPort(), 1.387 - req.getContextPath()); 1.388 - } 1.389 - 1.390 - private static String enforceExt(String filename, String ext) { 1.391 - return filename.endsWith(ext) ? filename : filename + ext; 1.392 - } 1.393 - 1.394 - private static String jspPath(String filename) { 1.395 - return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp"); 1.396 - } 1.397 - 1.398 - private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 1.399 - // the very first thing to do is to force UTF-8 1.400 - req.setCharacterEncoding("UTF-8"); 1.401 - 1.402 - // choose the requested language as session language (if available) or fall back to english, otherwise 1.403 - HttpSession session = req.getSession(); 1.404 - if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { 1.405 - Optional<List<String>> availableLanguages = availableLanguages().map(Arrays::asList); 1.406 - Optional<Locale> reqLocale = Optional.of(req.getLocale()); 1.407 - Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH); 1.408 - session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale); 1.409 - LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage()); 1.410 - } else { 1.411 - Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE); 1.412 - resp.setLocale(sessionLocale); 1.413 - LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale); 1.414 - } 1.415 - 1.416 - // set some internal request attributes 1.417 - final String fullPath = req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse(""); 1.418 - req.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref(req)); 1.419 - req.setAttribute(Constants.REQ_ATTR_PATH, fullPath); 1.420 - req.setAttribute(Constants.REQ_ATTR_RESOURCE_BUNDLE, getResourceBundleName()); 1.421 - 1.422 - // if this is an error path, bypass the normal flow 1.423 - if (fullPath.startsWith("/error/")) { 1.424 - final var mapping = findMapping(method, req); 1.425 - if (mapping.isPresent()) { 1.426 - invokeMapping(mapping.get(), req, resp, null); 1.427 - } 1.428 - return; 1.429 - } 1.430 - 1.431 - // obtain a connection and create the data access objects 1.432 - final var db = (DataSourceProvider) req.getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME()); 1.433 - final var ds = db.getDataSource(); 1.434 - if (ds == null) { 1.435 - resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details."); 1.436 - return; 1.437 - } 1.438 - try (final var connection = ds.getConnection()) { 1.439 - final var dao = createDataAccessObjects(connection); 1.440 - try { 1.441 - connection.setAutoCommit(false); 1.442 - // call the handler, if available, or send an HTTP 404 error 1.443 - final var mapping = findMapping(method, req); 1.444 - if (mapping.isPresent()) { 1.445 - invokeMapping(mapping.get(), req, resp, dao); 1.446 - } else { 1.447 - resp.sendError(HttpServletResponse.SC_NOT_FOUND); 1.448 - } 1.449 - connection.commit(); 1.450 - } catch (SQLException ex) { 1.451 - LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage()); 1.452 - LOG.debug("Details: ", ex); 1.453 - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code: " + ex.getErrorCode()); 1.454 - connection.rollback(); 1.455 - } 1.456 - } catch (SQLException ex) { 1.457 - LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage()); 1.458 - LOG.debug("Details: ", ex); 1.459 - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.getErrorCode()); 1.460 - } 1.461 - } 1.462 - 1.463 - @Override 1.464 - protected final void doGet(HttpServletRequest req, HttpServletResponse resp) 1.465 - throws ServletException, IOException { 1.466 - doProcess(HttpMethod.GET, req, resp); 1.467 - } 1.468 - 1.469 - @Override 1.470 - protected final void doPost(HttpServletRequest req, HttpServletResponse resp) 1.471 - throws ServletException, IOException { 1.472 - doProcess(HttpMethod.POST, req, resp); 1.473 - } 1.474 -}
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/src/main/java/de/uapcore/lightpit/AbstractServlet.java Tue Jan 05 19:19:31 2021 +0100 2.3 @@ -0,0 +1,471 @@ 2.4 +/* 2.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 2.6 + * 2.7 + * Copyright 2018 Mike Becker. All rights reserved. 2.8 + * 2.9 + * Redistribution and use in source and binary forms, with or without 2.10 + * modification, are permitted provided that the following conditions are met: 2.11 + * 2.12 + * 1. Redistributions of source code must retain the above copyright 2.13 + * notice, this list of conditions and the following disclaimer. 2.14 + * 2.15 + * 2. Redistributions in binary form must reproduce the above copyright 2.16 + * notice, this list of conditions and the following disclaimer in the 2.17 + * documentation and/or other materials provided with the distribution. 2.18 + * 2.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 2.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 2.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 2.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 2.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 2.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 2.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 2.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 2.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 2.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 2.29 + * POSSIBILITY OF SUCH DAMAGE. 2.30 + * 2.31 + */ 2.32 +package de.uapcore.lightpit; 2.33 + 2.34 +import de.uapcore.lightpit.dao.DataAccessObject; 2.35 +import de.uapcore.lightpit.dao.PostgresDataAccessObject; 2.36 +import org.slf4j.Logger; 2.37 +import org.slf4j.LoggerFactory; 2.38 + 2.39 +import javax.servlet.ServletException; 2.40 +import javax.servlet.http.HttpServlet; 2.41 +import javax.servlet.http.HttpServletRequest; 2.42 +import javax.servlet.http.HttpServletResponse; 2.43 +import javax.servlet.http.HttpSession; 2.44 +import java.io.IOException; 2.45 +import java.lang.reflect.*; 2.46 +import java.sql.Connection; 2.47 +import java.sql.SQLException; 2.48 +import java.util.*; 2.49 +import java.util.function.Function; 2.50 +import java.util.stream.Collectors; 2.51 + 2.52 +/** 2.53 + * A special implementation of a HTTPServlet which is focused on implementing 2.54 + * the necessary functionality for LightPIT pages. 2.55 + */ 2.56 +public abstract class AbstractServlet extends HttpServlet { 2.57 + 2.58 + private static final Logger LOG = LoggerFactory.getLogger(AbstractServlet.class); 2.59 + 2.60 + private static final String SITE_JSP = jspPath("site"); 2.61 + 2.62 + /** 2.63 + * Invocation mapping gathered from the {@link RequestMapping} annotations. 2.64 + * <p> 2.65 + * Paths in this map must always start with a leading slash, although 2.66 + * the specification in the annotation must not start with a leading slash. 2.67 + * <p> 2.68 + * The reason for this is the different handling of empty paths in 2.69 + * {@link HttpServletRequest#getPathInfo()}. 2.70 + */ 2.71 + private final Map<HttpMethod, Map<PathPattern, Method>> mappings = new HashMap<>(); 2.72 + 2.73 + /** 2.74 + * Returns the name of the resource bundle associated with this servlet. 2.75 + * 2.76 + * @return the resource bundle base name 2.77 + */ 2.78 + protected abstract String getResourceBundleName(); 2.79 + 2.80 + 2.81 + /** 2.82 + * Creates a set of data access objects for the specified connection. 2.83 + * 2.84 + * @param connection the SQL connection 2.85 + * @return a set of data access objects 2.86 + */ 2.87 + private DataAccessObject createDataAccessObjects(Connection connection) { 2.88 + final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME()); 2.89 + if (df.getDialect() == DataSourceProvider.Dialect.Postgres) { 2.90 + return new PostgresDataAccessObject(connection); 2.91 + } 2.92 + throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug."); 2.93 + } 2.94 + 2.95 + private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException { 2.96 + final var pathPattern = mapping.getKey(); 2.97 + final var method = mapping.getValue(); 2.98 + try { 2.99 + LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName()); 2.100 + final var paramTypes = method.getParameterTypes(); 2.101 + final var paramValues = new Object[paramTypes.length]; 2.102 + for (int i = 0; i < paramTypes.length; i++) { 2.103 + if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) { 2.104 + paramValues[i] = req; 2.105 + } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) { 2.106 + paramValues[i] = resp; 2.107 + } 2.108 + if (paramTypes[i].isAssignableFrom(DataAccessObject.class)) { 2.109 + paramValues[i] = dao; 2.110 + } 2.111 + if (paramTypes[i].isAssignableFrom(PathParameters.class)) { 2.112 + paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req)); 2.113 + } 2.114 + } 2.115 + method.invoke(this, paramValues); 2.116 + } catch (InvocationTargetException ex) { 2.117 + LOG.error("invocation of method {}::{} failed: {}", 2.118 + method.getDeclaringClass().getName(), method.getName(), ex.getTargetException().getMessage()); 2.119 + LOG.debug("Details: ", ex.getTargetException()); 2.120 + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getTargetException().getMessage()); 2.121 + } catch (ReflectiveOperationException | ClassCastException ex) { 2.122 + LOG.error("invocation of method {}::{} failed: {}", 2.123 + method.getDeclaringClass().getName(), method.getName(), ex.getMessage()); 2.124 + LOG.debug("Details: ", ex); 2.125 + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); 2.126 + } 2.127 + } 2.128 + 2.129 + @Override 2.130 + public void init() throws ServletException { 2.131 + scanForRequestMappings(); 2.132 + 2.133 + LOG.trace("{} initialized", getServletName()); 2.134 + } 2.135 + 2.136 + private void scanForRequestMappings() { 2.137 + try { 2.138 + Method[] methods = getClass().getDeclaredMethods(); 2.139 + for (Method method : methods) { 2.140 + Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class)); 2.141 + if (mapping.isPresent()) { 2.142 + if (mapping.get().requestPath().isBlank()) { 2.143 + LOG.warn("{} is annotated with {} but request path is empty", 2.144 + method.getName(), RequestMapping.class.getSimpleName() 2.145 + ); 2.146 + continue; 2.147 + } 2.148 + 2.149 + if (!Modifier.isPublic(method.getModifiers())) { 2.150 + LOG.warn("{} is annotated with {} but is not public", 2.151 + method.getName(), RequestMapping.class.getSimpleName() 2.152 + ); 2.153 + continue; 2.154 + } 2.155 + if (Modifier.isAbstract(method.getModifiers())) { 2.156 + LOG.warn("{} is annotated with {} but is abstract", 2.157 + method.getName(), RequestMapping.class.getSimpleName() 2.158 + ); 2.159 + continue; 2.160 + } 2.161 + 2.162 + boolean paramsInjectible = true; 2.163 + for (var param : method.getParameterTypes()) { 2.164 + paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param) 2.165 + || HttpServletResponse.class.isAssignableFrom(param) 2.166 + || PathParameters.class.isAssignableFrom(param) 2.167 + || DataAccessObject.class.isAssignableFrom(param); 2.168 + } 2.169 + if (paramsInjectible) { 2.170 + try { 2.171 + PathPattern pathPattern = new PathPattern(mapping.get().requestPath()); 2.172 + 2.173 + final var methodMappings = mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>()); 2.174 + final var currentMapping = methodMappings.putIfAbsent(pathPattern, method); 2.175 + if (currentMapping != null) { 2.176 + LOG.warn("Cannot map {} {} to {} in class {} - this would override the mapping to {}", 2.177 + mapping.get().method(), 2.178 + mapping.get().requestPath(), 2.179 + method.getName(), 2.180 + getClass().getSimpleName(), 2.181 + currentMapping.getName() 2.182 + ); 2.183 + } 2.184 + 2.185 + LOG.debug("{} {} maps to {}::{}", 2.186 + mapping.get().method(), 2.187 + mapping.get().requestPath(), 2.188 + getClass().getSimpleName(), 2.189 + method.getName() 2.190 + ); 2.191 + } catch (IllegalArgumentException ex) { 2.192 + LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid", 2.193 + method.getName(), mapping.get().requestPath() 2.194 + ); 2.195 + } 2.196 + } else { 2.197 + LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed", 2.198 + method.getName(), RequestMapping.class.getSimpleName() 2.199 + ); 2.200 + } 2.201 + } 2.202 + } 2.203 + } catch (SecurityException ex) { 2.204 + LOG.error("Scan for request mappings on declared methods failed.", ex); 2.205 + } 2.206 + } 2.207 + 2.208 + @Override 2.209 + public void destroy() { 2.210 + mappings.clear(); 2.211 + LOG.trace("{} destroyed", getServletName()); 2.212 + } 2.213 + 2.214 + /** 2.215 + * Sets the name of the content page. 2.216 + * <p> 2.217 + * It is sufficient to specify the name without any extension. The extension 2.218 + * is added automatically if not specified. 2.219 + * 2.220 + * @param req the servlet request object 2.221 + * @param pageName the name of the content page 2.222 + * @see Constants#REQ_ATTR_CONTENT_PAGE 2.223 + */ 2.224 + protected void setContentPage(HttpServletRequest req, String pageName) { 2.225 + req.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(pageName)); 2.226 + } 2.227 + 2.228 + /** 2.229 + * Sets the navigation menu. 2.230 + * 2.231 + * @param req the servlet request object 2.232 + * @param jspName the name of the menu's jsp file 2.233 + * @see Constants#REQ_ATTR_NAVIGATION 2.234 + */ 2.235 + protected void setNavigationMenu(HttpServletRequest req, String jspName) { 2.236 + req.setAttribute(Constants.REQ_ATTR_NAVIGATION, jspPath(jspName)); 2.237 + } 2.238 + 2.239 + /** 2.240 + * @param req the servlet request object 2.241 + * @param location the location where to redirect 2.242 + * @see Constants#REQ_ATTR_REDIRECT_LOCATION 2.243 + */ 2.244 + protected void setRedirectLocation(HttpServletRequest req, String location) { 2.245 + if (location.startsWith("./")) { 2.246 + location = location.replaceFirst("\\./", baseHref(req)); 2.247 + } 2.248 + req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location); 2.249 + } 2.250 + 2.251 + /** 2.252 + * Specifies the names of additional stylesheets used by this Servlet. 2.253 + * <p> 2.254 + * It is sufficient to specify the name without any extension. The extension 2.255 + * is added automatically if not specified. 2.256 + * 2.257 + * @param req the servlet request object 2.258 + * @param stylesheets the names of the stylesheets 2.259 + */ 2.260 + public void setStylesheet(HttpServletRequest req, String ... stylesheets) { 2.261 + req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Arrays 2.262 + .stream(stylesheets) 2.263 + .map(s -> enforceExt(s, ".css")) 2.264 + .collect(Collectors.toUnmodifiableList())); 2.265 + } 2.266 + 2.267 + /** 2.268 + * Sets the view model object. 2.269 + * The type must match the expected type in the JSP file. 2.270 + * 2.271 + * @param req the servlet request object 2.272 + * @param viewModel the view model object 2.273 + */ 2.274 + public void setViewModel(HttpServletRequest req, Object viewModel) { 2.275 + req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel); 2.276 + } 2.277 + 2.278 + private <T> Optional<T> parseParameter(String paramValue, Class<T> clazz) { 2.279 + if (paramValue == null) return Optional.empty(); 2.280 + if (clazz.equals(Boolean.class)) { 2.281 + if (paramValue.equalsIgnoreCase("false") || paramValue.equals("0")) { 2.282 + return Optional.of((T) Boolean.FALSE); 2.283 + } else { 2.284 + return Optional.of((T) Boolean.TRUE); 2.285 + } 2.286 + } 2.287 + if (clazz.equals(String.class)) return Optional.of((T) paramValue); 2.288 + if (java.sql.Date.class.isAssignableFrom(clazz)) { 2.289 + try { 2.290 + return Optional.of((T) java.sql.Date.valueOf(paramValue)); 2.291 + } catch (IllegalArgumentException ex) { 2.292 + return Optional.empty(); 2.293 + } 2.294 + } 2.295 + try { 2.296 + final Constructor<T> ctor = clazz.getConstructor(String.class); 2.297 + return Optional.of(ctor.newInstance(paramValue)); 2.298 + } catch (ReflectiveOperationException e) { 2.299 + // does not type check and is not convertible - treat as if the parameter was never set 2.300 + return Optional.empty(); 2.301 + } 2.302 + } 2.303 + 2.304 + /** 2.305 + * Obtains a request parameter of the specified type. 2.306 + * The specified type must have a single-argument constructor accepting a string to perform conversion. 2.307 + * The constructor of the specified type may throw an exception on conversion failures. 2.308 + * 2.309 + * @param req the servlet request object 2.310 + * @param clazz the class object of the expected type 2.311 + * @param name the name of the parameter 2.312 + * @param <T> the expected type 2.313 + * @return the parameter value or an empty optional, if no parameter with the specified name was found 2.314 + */ 2.315 + protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) { 2.316 + if (clazz.isArray()) { 2.317 + final String[] paramValues = req.getParameterValues(name); 2.318 + int len = paramValues == null ? 0 : paramValues.length; 2.319 + final var array = (T) Array.newInstance(clazz.getComponentType(), len); 2.320 + for (int i = 0; i < len; i++) { 2.321 + try { 2.322 + final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class); 2.323 + Array.set(array, i, ctor.newInstance(paramValues[i])); 2.324 + } catch (ReflectiveOperationException e) { 2.325 + throw new RuntimeException(e); 2.326 + } 2.327 + } 2.328 + return Optional.of(array); 2.329 + } else { 2.330 + return parseParameter(req.getParameter(name), clazz); 2.331 + } 2.332 + } 2.333 + 2.334 + /** 2.335 + * Tries to look up an entity with a key obtained from a request parameter. 2.336 + * 2.337 + * @param req the servlet request object 2.338 + * @param clazz the class representing the type of the request parameter 2.339 + * @param name the name of the request parameter 2.340 + * @param find the find function (typically a DAO function) 2.341 + * @param <T> the type of the request parameter 2.342 + * @param <R> the type of the looked up entity 2.343 + * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing 2.344 + * @throws SQLException if the find function throws an exception 2.345 + */ 2.346 + protected <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, Function<? super T, ? extends R> find) { 2.347 + final var param = getParameter(req, clazz, name); 2.348 + if (param.isPresent()) { 2.349 + return Optional.ofNullable(find.apply(param.get())); 2.350 + } else { 2.351 + return Optional.empty(); 2.352 + } 2.353 + } 2.354 + 2.355 + protected void setAttributeFromParameter(HttpServletRequest req, String name) { 2.356 + final var parm = req.getParameter(name); 2.357 + if (parm != null) { 2.358 + req.setAttribute(name, parm); 2.359 + } 2.360 + } 2.361 + 2.362 + private String sanitizeRequestPath(HttpServletRequest req) { 2.363 + return Optional.ofNullable(req.getPathInfo()).orElse("/"); 2.364 + } 2.365 + 2.366 + private Optional<Map.Entry<PathPattern, Method>> findMapping(HttpMethod method, HttpServletRequest req) { 2.367 + return Optional.ofNullable(mappings.get(method)).flatMap(rm -> 2.368 + rm.entrySet().stream().filter( 2.369 + kv -> kv.getKey().matches(sanitizeRequestPath(req)) 2.370 + ).findAny() 2.371 + ); 2.372 + } 2.373 + 2.374 + protected void renderSite(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 2.375 + req.getRequestDispatcher(SITE_JSP).forward(req, resp); 2.376 + } 2.377 + 2.378 + protected Optional<String[]> availableLanguages() { 2.379 + return Optional.ofNullable(getServletContext().getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*")); 2.380 + } 2.381 + 2.382 + private static String baseHref(HttpServletRequest req) { 2.383 + return String.format("%s://%s:%d%s/", 2.384 + req.getScheme(), 2.385 + req.getServerName(), 2.386 + req.getServerPort(), 2.387 + req.getContextPath()); 2.388 + } 2.389 + 2.390 + private static String enforceExt(String filename, String ext) { 2.391 + return filename.endsWith(ext) ? filename : filename + ext; 2.392 + } 2.393 + 2.394 + private static String jspPath(String filename) { 2.395 + return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp"); 2.396 + } 2.397 + 2.398 + private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 2.399 + // the very first thing to do is to force UTF-8 2.400 + req.setCharacterEncoding("UTF-8"); 2.401 + 2.402 + // choose the requested language as session language (if available) or fall back to english, otherwise 2.403 + HttpSession session = req.getSession(); 2.404 + if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { 2.405 + Optional<List<String>> availableLanguages = availableLanguages().map(Arrays::asList); 2.406 + Optional<Locale> reqLocale = Optional.of(req.getLocale()); 2.407 + Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH); 2.408 + session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale); 2.409 + LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage()); 2.410 + } else { 2.411 + Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE); 2.412 + resp.setLocale(sessionLocale); 2.413 + LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale); 2.414 + } 2.415 + 2.416 + // set some internal request attributes 2.417 + final String fullPath = req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse(""); 2.418 + req.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref(req)); 2.419 + req.setAttribute(Constants.REQ_ATTR_PATH, fullPath); 2.420 + req.setAttribute(Constants.REQ_ATTR_RESOURCE_BUNDLE, getResourceBundleName()); 2.421 + 2.422 + // if this is an error path, bypass the normal flow 2.423 + if (fullPath.startsWith("/error/")) { 2.424 + final var mapping = findMapping(method, req); 2.425 + if (mapping.isPresent()) { 2.426 + invokeMapping(mapping.get(), req, resp, null); 2.427 + } 2.428 + return; 2.429 + } 2.430 + 2.431 + // obtain a connection and create the data access objects 2.432 + final var db = (DataSourceProvider) req.getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME()); 2.433 + final var ds = db.getDataSource(); 2.434 + if (ds == null) { 2.435 + resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details."); 2.436 + return; 2.437 + } 2.438 + try (final var connection = ds.getConnection()) { 2.439 + final var dao = createDataAccessObjects(connection); 2.440 + try { 2.441 + connection.setAutoCommit(false); 2.442 + // call the handler, if available, or send an HTTP 404 error 2.443 + final var mapping = findMapping(method, req); 2.444 + if (mapping.isPresent()) { 2.445 + invokeMapping(mapping.get(), req, resp, dao); 2.446 + } else { 2.447 + resp.sendError(HttpServletResponse.SC_NOT_FOUND); 2.448 + } 2.449 + connection.commit(); 2.450 + } catch (SQLException ex) { 2.451 + LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage()); 2.452 + LOG.debug("Details: ", ex); 2.453 + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code: " + ex.getErrorCode()); 2.454 + connection.rollback(); 2.455 + } 2.456 + } catch (SQLException ex) { 2.457 + LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage()); 2.458 + LOG.debug("Details: ", ex); 2.459 + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.getErrorCode()); 2.460 + } 2.461 + } 2.462 + 2.463 + @Override 2.464 + protected final void doGet(HttpServletRequest req, HttpServletResponse resp) 2.465 + throws ServletException, IOException { 2.466 + doProcess(HttpMethod.GET, req, resp); 2.467 + } 2.468 + 2.469 + @Override 2.470 + protected final void doPost(HttpServletRequest req, HttpServletResponse resp) 2.471 + throws ServletException, IOException { 2.472 + doProcess(HttpMethod.POST, req, resp); 2.473 + } 2.474 +}
3.1 --- a/src/main/java/de/uapcore/lightpit/HttpMethod.java Mon Jan 04 17:30:10 2021 +0100 3.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 3.3 @@ -1,34 +0,0 @@ 3.4 -/* 3.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 3.6 - * 3.7 - * Copyright 2018 Mike Becker. All rights reserved. 3.8 - * 3.9 - * Redistribution and use in source and binary forms, with or without 3.10 - * modification, are permitted provided that the following conditions are met: 3.11 - * 3.12 - * 1. Redistributions of source code must retain the above copyright 3.13 - * notice, this list of conditions and the following disclaimer. 3.14 - * 3.15 - * 2. Redistributions in binary form must reproduce the above copyright 3.16 - * notice, this list of conditions and the following disclaimer in the 3.17 - * documentation and/or other materials provided with the distribution. 3.18 - * 3.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 3.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 3.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 3.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 3.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 3.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 3.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 3.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 3.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 3.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 3.29 - * POSSIBILITY OF SUCH DAMAGE. 3.30 - * 3.31 - */ 3.32 -package de.uapcore.lightpit; 3.33 - 3.34 - 3.35 -public enum HttpMethod { 3.36 - GET, POST, PUT, DELETE, TRACE, HEAD, OPTIONS 3.37 -}
4.1 --- a/src/main/java/de/uapcore/lightpit/PathParameters.java Mon Jan 04 17:30:10 2021 +0100 4.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 4.3 @@ -1,6 +0,0 @@ 4.4 -package de.uapcore.lightpit; 4.5 - 4.6 -import java.util.HashMap; 4.7 - 4.8 -public class PathParameters extends HashMap<String, String> { 4.9 -}
5.1 --- a/src/main/java/de/uapcore/lightpit/PathPattern.java Mon Jan 04 17:30:10 2021 +0100 5.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 5.3 @@ -1,125 +0,0 @@ 5.4 -package de.uapcore.lightpit; 5.5 - 5.6 -import java.util.ArrayList; 5.7 -import java.util.List; 5.8 - 5.9 -public final class PathPattern { 5.10 - 5.11 - private final List<String> nodePatterns; 5.12 - private final boolean collection; 5.13 - 5.14 - /** 5.15 - * Constructs a new path pattern. 5.16 - * The special directories . and .. are disallowed in the pattern. 5.17 - * 5.18 - * @param pattern 5.19 - */ 5.20 - public PathPattern(String pattern) { 5.21 - nodePatterns = parse(pattern); 5.22 - collection = pattern.endsWith("/"); 5.23 - } 5.24 - 5.25 - private List<String> parse(String pattern) { 5.26 - 5.27 - var nodes = new ArrayList<String>(); 5.28 - var parts = pattern.split("/"); 5.29 - 5.30 - for (var part : parts) { 5.31 - if (part.isBlank()) continue; 5.32 - if (part.equals(".") || part.equals("..")) 5.33 - throw new IllegalArgumentException("Path must not contain '.' or '..' nodes."); 5.34 - nodes.add(part); 5.35 - } 5.36 - 5.37 - return nodes; 5.38 - } 5.39 - 5.40 - /** 5.41 - * Matches a path against this pattern. 5.42 - * The path must be canonical in the sense that no . or .. parts occur. 5.43 - * 5.44 - * @param path the path to match 5.45 - * @return true if the path matches the pattern, false otherwise 5.46 - */ 5.47 - public boolean matches(String path) { 5.48 - if (collection ^ path.endsWith("/")) 5.49 - return false; 5.50 - 5.51 - var nodes = parse(path); 5.52 - if (nodePatterns.size() != nodes.size()) 5.53 - return false; 5.54 - 5.55 - for (int i = 0 ; i < nodePatterns.size() ; i++) { 5.56 - var pattern = nodePatterns.get(i); 5.57 - var node = nodes.get(i); 5.58 - if (pattern.startsWith("$")) 5.59 - continue; 5.60 - if (!pattern.equals(node)) 5.61 - return false; 5.62 - } 5.63 - 5.64 - return true; 5.65 - } 5.66 - 5.67 - /** 5.68 - * Returns the path parameters found in the specified path using this pattern. 5.69 - * The return value of this method is undefined, if the patter does not match. 5.70 - * 5.71 - * @param path the path 5.72 - * @return the path parameters, if any, or an empty map 5.73 - * @see #matches(String) 5.74 - */ 5.75 - public PathParameters obtainPathParameters(String path) { 5.76 - var params = new PathParameters(); 5.77 - 5.78 - var nodes = parse(path); 5.79 - 5.80 - for (int i = 0 ; i < Math.min(nodes.size(), nodePatterns.size()) ; i++) { 5.81 - var pattern = nodePatterns.get(i); 5.82 - var node = nodes.get(i); 5.83 - if (pattern.startsWith("$")) { 5.84 - params.put(pattern.substring(1), node); 5.85 - } 5.86 - } 5.87 - 5.88 - return params; 5.89 - } 5.90 - 5.91 - @Override 5.92 - public int hashCode() { 5.93 - var str = new StringBuilder(); 5.94 - for (var node : nodePatterns) { 5.95 - if (node.startsWith("$")) { 5.96 - str.append("/$"); 5.97 - } else { 5.98 - str.append('/'); 5.99 - str.append(node); 5.100 - } 5.101 - } 5.102 - if (collection) 5.103 - str.append('/'); 5.104 - 5.105 - return str.toString().hashCode(); 5.106 - } 5.107 - 5.108 - @Override 5.109 - public boolean equals(Object obj) { 5.110 - if (!obj.getClass().equals(PathPattern.class)) 5.111 - return false; 5.112 - 5.113 - var other = (PathPattern) obj; 5.114 - if (collection ^ other.collection || nodePatterns.size() != other.nodePatterns.size()) 5.115 - return false; 5.116 - 5.117 - for (int i = 0 ; i < nodePatterns.size() ; i++) { 5.118 - var left = nodePatterns.get(i); 5.119 - var right = other.nodePatterns.get(i); 5.120 - if (left.startsWith("$") && right.startsWith("$")) 5.121 - continue; 5.122 - if (!left.equals(right)) 5.123 - return false; 5.124 - } 5.125 - 5.126 - return true; 5.127 - } 5.128 -}
6.1 --- a/src/main/java/de/uapcore/lightpit/RequestMapping.java Mon Jan 04 17:30:10 2021 +0100 6.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 6.3 @@ -1,62 +0,0 @@ 6.4 -/* 6.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 6.6 - * 6.7 - * Copyright 2018 Mike Becker. All rights reserved. 6.8 - * 6.9 - * Redistribution and use in source and binary forms, with or without 6.10 - * modification, are permitted provided that the following conditions are met: 6.11 - * 6.12 - * 1. Redistributions of source code must retain the above copyright 6.13 - * notice, this list of conditions and the following disclaimer. 6.14 - * 6.15 - * 2. Redistributions in binary form must reproduce the above copyright 6.16 - * notice, this list of conditions and the following disclaimer in the 6.17 - * documentation and/or other materials provided with the distribution. 6.18 - * 6.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 6.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 6.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 6.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 6.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 6.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 6.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 6.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 6.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 6.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 6.29 - * POSSIBILITY OF SUCH DAMAGE. 6.30 - * 6.31 - */ 6.32 -package de.uapcore.lightpit; 6.33 - 6.34 -import java.lang.annotation.*; 6.35 - 6.36 - 6.37 -/** 6.38 - * Maps requests to methods. 6.39 - * <p> 6.40 - * This annotation is used to annotate methods within classes which 6.41 - * override {@link AbstractLightPITServlet}. 6.42 - */ 6.43 -@Documented 6.44 -@Retention(RetentionPolicy.RUNTIME) 6.45 -@Target(ElementType.METHOD) 6.46 -public @interface RequestMapping { 6.47 - 6.48 - /** 6.49 - * Specifies the HTTP method. 6.50 - * 6.51 - * @return the HTTP method handled by the annotated Java method 6.52 - */ 6.53 - HttpMethod method(); 6.54 - 6.55 - /** 6.56 - * Specifies the request path relative to the module path. 6.57 - * The trailing slash is important. 6.58 - * A node may start with a dollar ($) sign. 6.59 - * This part of the path is then treated as an path parameter. 6.60 - * Path parameters can be obtained by including the {@link PathParameters} interface in the signature. 6.61 - * 6.62 - * @return the request path the annotated method should handle 6.63 - */ 6.64 - String requestPath() default "/"; 6.65 -}
7.1 --- a/src/main/java/de/uapcore/lightpit/ResourceKey.java Mon Jan 04 17:30:10 2021 +0100 7.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 7.3 @@ -1,74 +0,0 @@ 7.4 -/* 7.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 7.6 - * 7.7 - * Copyright 2018 Mike Becker. All rights reserved. 7.8 - * 7.9 - * Redistribution and use in source and binary forms, with or without 7.10 - * modification, are permitted provided that the following conditions are met: 7.11 - * 7.12 - * 1. Redistributions of source code must retain the above copyright 7.13 - * notice, this list of conditions and the following disclaimer. 7.14 - * 7.15 - * 2. Redistributions in binary form must reproduce the above copyright 7.16 - * notice, this list of conditions and the following disclaimer in the 7.17 - * documentation and/or other materials provided with the distribution. 7.18 - * 7.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 7.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 7.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 7.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 7.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 7.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 7.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 7.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 7.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 7.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 7.29 - * POSSIBILITY OF SUCH DAMAGE. 7.30 - * 7.31 - */ 7.32 -package de.uapcore.lightpit; 7.33 - 7.34 -import java.util.Objects; 7.35 - 7.36 -/** 7.37 - * Fully specifies a resource key by the bundle and the key name. 7.38 - */ 7.39 -public final class ResourceKey { 7.40 - private String bundle; 7.41 - private String key; 7.42 - 7.43 - public ResourceKey(String bundle, String key) { 7.44 - this.bundle = bundle; 7.45 - this.key = key; 7.46 - } 7.47 - 7.48 - public void setBundle(String bundle) { 7.49 - this.bundle = bundle; 7.50 - } 7.51 - 7.52 - public String getBundle() { 7.53 - return bundle; 7.54 - } 7.55 - 7.56 - public void setKey(String key) { 7.57 - this.key = key; 7.58 - } 7.59 - 7.60 - public String getKey() { 7.61 - return key; 7.62 - } 7.63 - 7.64 - @Override 7.65 - public boolean equals(Object o) { 7.66 - if (this == o) return true; 7.67 - if (o == null || getClass() != o.getClass()) return false; 7.68 - ResourceKey that = (ResourceKey) o; 7.69 - return bundle.equals(that.bundle) && 7.70 - key.equals(that.key); 7.71 - } 7.72 - 7.73 - @Override 7.74 - public int hashCode() { 7.75 - return Objects.hash(bundle, key); 7.76 - } 7.77 -}
8.1 --- a/src/main/java/de/uapcore/lightpit/modules/ErrorModule.java Mon Jan 04 17:30:10 2021 +0100 8.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ErrorModule.java Tue Jan 05 19:19:31 2021 +0100 8.3 @@ -28,7 +28,7 @@ 8.4 */ 8.5 package de.uapcore.lightpit.modules; 8.6 8.7 -import de.uapcore.lightpit.AbstractLightPITServlet; 8.8 +import de.uapcore.lightpit.AbstractServlet; 8.9 import de.uapcore.lightpit.HttpMethod; 8.10 import de.uapcore.lightpit.RequestMapping; 8.11 8.12 @@ -43,7 +43,7 @@ 8.13 name = "ErrorModule", 8.14 urlPatterns = "/error/*" 8.15 ) 8.16 -public final class ErrorModule extends AbstractLightPITServlet { 8.17 +public final class ErrorModule extends AbstractServlet { 8.18 8.19 public static final String REQ_ATTR_RETURN_LINK = "returnLink"; 8.20
9.1 --- a/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java Mon Jan 04 17:30:10 2021 +0100 9.2 +++ b/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java Tue Jan 05 19:19:31 2021 +0100 9.3 @@ -28,7 +28,7 @@ 9.4 */ 9.5 package de.uapcore.lightpit.modules; 9.6 9.7 -import de.uapcore.lightpit.AbstractLightPITServlet; 9.8 +import de.uapcore.lightpit.AbstractServlet; 9.9 import de.uapcore.lightpit.Constants; 9.10 import de.uapcore.lightpit.HttpMethod; 9.11 import de.uapcore.lightpit.RequestMapping; 9.12 @@ -47,7 +47,7 @@ 9.13 name = "LanguageModule", 9.14 urlPatterns = "/language/*" 9.15 ) 9.16 -public final class LanguageModule extends AbstractLightPITServlet { 9.17 +public final class LanguageModule extends AbstractServlet { 9.18 9.19 private static final Logger LOG = LoggerFactory.getLogger(LanguageModule.class); 9.20
10.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Mon Jan 04 17:30:10 2021 +0100 10.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Tue Jan 05 19:19:31 2021 +0100 10.3 @@ -61,7 +61,7 @@ 10.4 name = "ProjectsModule", 10.5 urlPatterns = "/projects/*" 10.6 ) 10.7 -public final class ProjectsModule extends AbstractLightPITServlet { 10.8 +public final class ProjectsModule extends AbstractServlet { 10.9 10.10 private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class); 10.11
11.1 --- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java Mon Jan 04 17:30:10 2021 +0100 11.2 +++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java Tue Jan 05 19:19:31 2021 +0100 11.3 @@ -28,7 +28,7 @@ 11.4 */ 11.5 package de.uapcore.lightpit.modules; 11.6 11.7 -import de.uapcore.lightpit.AbstractLightPITServlet; 11.8 +import de.uapcore.lightpit.AbstractServlet; 11.9 import de.uapcore.lightpit.Constants; 11.10 import de.uapcore.lightpit.HttpMethod; 11.11 import de.uapcore.lightpit.RequestMapping; 11.12 @@ -51,7 +51,7 @@ 11.13 name = "UsersModule", 11.14 urlPatterns = "/teams/*" 11.15 ) 11.16 -public final class UsersModule extends AbstractLightPITServlet { 11.17 +public final class UsersModule extends AbstractServlet { 11.18 11.19 private static final Logger LOG = LoggerFactory.getLogger(UsersModule.class); 11.20
12.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 12.2 +++ b/src/main/kotlin/de/uapcore/lightpit/HttpMethod.kt Tue Jan 05 19:19:31 2021 +0100 12.3 @@ -0,0 +1,30 @@ 12.4 +/* 12.5 + * Copyright 2021 Mike Becker. All rights reserved. 12.6 + * 12.7 + * Redistribution and use in source and binary forms, with or without 12.8 + * modification, are permitted provided that the following conditions are met: 12.9 + * 12.10 + * 1. Redistributions of source code must retain the above copyright 12.11 + * notice, this list of conditions and the following disclaimer. 12.12 + * 12.13 + * 2. Redistributions in binary form must reproduce the above copyright 12.14 + * notice, this list of conditions and the following disclaimer in the 12.15 + * documentation and/or other materials provided with the distribution. 12.16 + * 12.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 12.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 12.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 12.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 12.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 12.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 12.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 12.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 12.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 12.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12.27 + */ 12.28 + 12.29 +package de.uapcore.lightpit 12.30 + 12.31 +enum class HttpMethod { 12.32 + GET, POST, PUT, DELETE, TRACE, HEAD, OPTIONS 12.33 +}
13.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 13.2 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Tue Jan 05 19:19:31 2021 +0100 13.3 @@ -0,0 +1,155 @@ 13.4 +/* 13.5 + * Copyright 2021 Mike Becker. All rights reserved. 13.6 + * 13.7 + * Redistribution and use in source and binary forms, with or without 13.8 + * modification, are permitted provided that the following conditions are met: 13.9 + * 13.10 + * 1. Redistributions of source code must retain the above copyright 13.11 + * notice, this list of conditions and the following disclaimer. 13.12 + * 13.13 + * 2. Redistributions in binary form must reproduce the above copyright 13.14 + * notice, this list of conditions and the following disclaimer in the 13.15 + * documentation and/or other materials provided with the distribution. 13.16 + * 13.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 13.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 13.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 13.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 13.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 13.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 13.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 13.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 13.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 13.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13.27 + */ 13.28 + 13.29 +package de.uapcore.lightpit 13.30 + 13.31 +import kotlin.math.min 13.32 + 13.33 +/** 13.34 + * Maps requests to methods. 13.35 + * 13.36 + * This annotation is used to annotate methods within classes which 13.37 + * override [AbstractServlet]. 13.38 + */ 13.39 +@MustBeDocumented 13.40 +@Retention(AnnotationRetention.RUNTIME) 13.41 +@Target(AnnotationTarget.FUNCTION) 13.42 +annotation class RequestMapping( 13.43 + 13.44 + /** 13.45 + * Specifies the HTTP method. 13.46 + * 13.47 + * @return the HTTP method handled by the annotated Java method 13.48 + */ 13.49 + val method: HttpMethod, 13.50 + 13.51 + /** 13.52 + * Specifies the request path relative to the module path. 13.53 + * The trailing slash is important. 13.54 + * A node may start with a dollar ($) sign. 13.55 + * This part of the path is then treated as an path parameter. 13.56 + * Path parameters can be obtained by including the [PathParameters] type in the signature. 13.57 + * 13.58 + * @return the request path the annotated method should handle 13.59 + */ 13.60 + val requestPath: String = "/" 13.61 +) 13.62 + 13.63 +class PathParameters : HashMap<String, String>() 13.64 + 13.65 +/** 13.66 + * A path pattern optionally containing placeholders. 13.67 + * 13.68 + * The special directories . and .. are disallowed in the pattern. 13.69 + * Placeholders start with a $ sign. 13.70 + * 13.71 + * @param pattern the pattern 13.72 + */ 13.73 +class PathPattern(pattern: String) { 13.74 + private val nodePatterns: List<String> 13.75 + private val collection: Boolean 13.76 + 13.77 + private fun parse(pattern: String): List<String> { 13.78 + val nodes = pattern.split("/").filter { it.isNotBlank() }.toList() 13.79 + require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." } 13.80 + return nodes 13.81 + } 13.82 + 13.83 + /** 13.84 + * Matches a path against this pattern. 13.85 + * The path must be canonical in the sense that no . or .. parts occur. 13.86 + * 13.87 + * @param path the path to match 13.88 + * @return true if the path matches the pattern, false otherwise 13.89 + */ 13.90 + fun matches(path: String): Boolean { 13.91 + if (collection xor path.endsWith("/")) return false 13.92 + val nodes = parse(path) 13.93 + if (nodePatterns.size != nodes.size) return false 13.94 + for (i in nodePatterns.indices) { 13.95 + val pattern = nodePatterns[i] 13.96 + val node = nodes[i] 13.97 + if (pattern.startsWith("$")) continue 13.98 + if (pattern != node) return false 13.99 + } 13.100 + return true 13.101 + } 13.102 + 13.103 + /** 13.104 + * Returns the path parameters found in the specified path using this pattern. 13.105 + * The return value of this method is undefined, if the patter does not match. 13.106 + * 13.107 + * @param path the path 13.108 + * @return the path parameters, if any, or an empty map 13.109 + * @see .matches 13.110 + */ 13.111 + fun obtainPathParameters(path: String): PathParameters { 13.112 + val params = PathParameters() 13.113 + val nodes = parse(path) 13.114 + for (i in 0 until min(nodes.size, nodePatterns.size)) { 13.115 + val pattern = nodePatterns[i] 13.116 + val node = nodes[i] 13.117 + if (pattern.startsWith("$")) { 13.118 + params[pattern.substring(1)] = node 13.119 + } 13.120 + } 13.121 + return params 13.122 + } 13.123 + 13.124 + override fun hashCode(): Int { 13.125 + val str = StringBuilder() 13.126 + for (node in nodePatterns) { 13.127 + if (node.startsWith("$")) { 13.128 + str.append("/$") 13.129 + } else { 13.130 + str.append('/') 13.131 + str.append(node) 13.132 + } 13.133 + } 13.134 + if (collection) str.append('/') 13.135 + return str.toString().hashCode() 13.136 + } 13.137 + 13.138 + override fun equals(other: Any?): Boolean { 13.139 + if (other is PathPattern) { 13.140 + if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false 13.141 + for (i in nodePatterns.indices) { 13.142 + val left = nodePatterns[i] 13.143 + val right = other.nodePatterns[i] 13.144 + if (left.startsWith("$") && right.startsWith("$")) continue 13.145 + if (left != right) return false 13.146 + } 13.147 + return true 13.148 + } else { 13.149 + return false 13.150 + } 13.151 + } 13.152 + 13.153 + init { 13.154 + nodePatterns = parse(pattern) 13.155 + collection = pattern.endsWith("/") 13.156 + } 13.157 +} 13.158 +