src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java

Sat, 16 May 2020 15:45:06 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 16 May 2020 15:45:06 +0200
changeset 53
6a8498291606
parent 47
57cfb94ab99f
child 54
77e01cda5a40
permissions
-rw-r--r--

fixes bug where displaying an error page for missing data source would also require that data source (error pages don't try to get database connections now)

also improves error pages in general

universe@7 1 /*
universe@7 2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
universe@34 3 *
universe@24 4 * Copyright 2018 Mike Becker. All rights reserved.
universe@34 5 *
universe@7 6 * Redistribution and use in source and binary forms, with or without
universe@7 7 * modification, are permitted provided that the following conditions are met:
universe@7 8 *
universe@7 9 * 1. Redistributions of source code must retain the above copyright
universe@7 10 * notice, this list of conditions and the following disclaimer.
universe@7 11 *
universe@7 12 * 2. Redistributions in binary form must reproduce the above copyright
universe@7 13 * notice, this list of conditions and the following disclaimer in the
universe@7 14 * documentation and/or other materials provided with the distribution.
universe@7 15 *
universe@7 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
universe@7 17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
universe@7 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
universe@7 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
universe@7 20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
universe@7 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
universe@7 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
universe@7 23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
universe@7 24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
universe@7 25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
universe@7 26 * POSSIBILITY OF SUCH DAMAGE.
universe@34 27 *
universe@7 28 */
universe@7 29 package de.uapcore.lightpit;
universe@7 30
universe@38 31 import de.uapcore.lightpit.dao.DataAccessObjects;
universe@38 32 import de.uapcore.lightpit.dao.postgres.PGDataAccessObjects;
universe@33 33 import org.slf4j.Logger;
universe@33 34 import org.slf4j.LoggerFactory;
universe@33 35
universe@7 36 import javax.servlet.ServletException;
universe@7 37 import javax.servlet.http.HttpServlet;
universe@7 38 import javax.servlet.http.HttpServletRequest;
universe@7 39 import javax.servlet.http.HttpServletResponse;
universe@13 40 import javax.servlet.http.HttpSession;
universe@33 41 import java.io.IOException;
universe@47 42 import java.lang.reflect.Constructor;
universe@33 43 import java.lang.reflect.Method;
universe@33 44 import java.lang.reflect.Modifier;
universe@38 45 import java.sql.Connection;
universe@38 46 import java.sql.SQLException;
universe@33 47 import java.util.*;
universe@7 48
universe@7 49 /**
universe@7 50 * A special implementation of a HTTPServlet which is focused on implementing
universe@7 51 * the necessary functionality for {@link LightPITModule}s.
universe@7 52 */
universe@9 53 public abstract class AbstractLightPITServlet extends HttpServlet {
universe@34 54
universe@10 55 private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class);
universe@34 56
universe@43 57 private static final String SITE_JSP = Functions.jspPath("site");
universe@33 58
universe@11 59 /**
universe@11 60 * The EL proxy is necessary, because the EL resolver cannot handle annotation properties.
universe@11 61 */
universe@36 62 private LightPITModule.ELProxy moduleInfo = null;
universe@33 63
universe@10 64 /**
universe@11 65 * Invocation mapping gathered from the {@link RequestMapping} annotations.
universe@34 66 * <p>
universe@18 67 * Paths in this map must always start with a leading slash, although
universe@18 68 * the specification in the annotation must not start with a leading slash.
universe@34 69 * <p>
universe@34 70 * The reason for this is the different handling of empty paths in
universe@18 71 * {@link HttpServletRequest#getPathInfo()}.
universe@11 72 */
universe@39 73 private final Map<HttpMethod, Map<String, Method>> mappings = new HashMap<>();
universe@11 74
universe@45 75 private final List<MenuEntry> subMenu = new ArrayList<>();
universe@45 76
universe@11 77 /**
universe@10 78 * Gives implementing modules access to the {@link ModuleManager}.
universe@33 79 *
universe@10 80 * @return the module manager
universe@10 81 */
universe@10 82 protected final ModuleManager getModuleManager() {
universe@10 83 return (ModuleManager) getServletContext().getAttribute(ModuleManager.SC_ATTR_NAME);
universe@10 84 }
universe@33 85
universe@38 86
universe@34 87 /**
universe@38 88 * Creates a set of data access objects for the specified connection.
universe@33 89 *
universe@38 90 * @param connection the SQL connection
universe@38 91 * @return a set of data access objects
universe@17 92 */
universe@38 93 private DataAccessObjects createDataAccessObjects(Connection connection) throws SQLException {
universe@38 94 final var df = (DatabaseFacade) getServletContext().getAttribute(DatabaseFacade.SC_ATTR_NAME);
universe@39 95 if (df.getSQLDialect() == DatabaseFacade.Dialect.Postgres) {
universe@39 96 return new PGDataAccessObjects(connection);
universe@38 97 }
universe@39 98 throw new AssertionError("Non-exhaustive if-else - this is a bug.");
universe@17 99 }
universe@33 100
universe@38 101 private ResponseType invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
universe@11 102 try {
universe@14 103 LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName());
universe@42 104 final var paramTypes = method.getParameterTypes();
universe@42 105 final var paramValues = new Object[paramTypes.length];
universe@42 106 for (int i = 0; i < paramTypes.length; i++) {
universe@42 107 if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) {
universe@42 108 paramValues[i] = req;
universe@42 109 } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) {
universe@42 110 paramValues[i] = resp;
universe@42 111 }
universe@42 112 if (paramTypes[i].isAssignableFrom(DataAccessObjects.class)) {
universe@42 113 paramValues[i] = dao;
universe@42 114 }
universe@42 115 }
universe@42 116 return (ResponseType) method.invoke(this, paramValues);
universe@12 117 } catch (ReflectiveOperationException | ClassCastException ex) {
universe@38 118 LOG.error("invocation of method {} failed: {}", method.getName(), ex.getMessage());
universe@38 119 LOG.debug("Details: ", ex);
universe@12 120 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
universe@12 121 return ResponseType.NONE;
universe@11 122 }
universe@11 123 }
universe@11 124
universe@11 125 @Override
universe@11 126 public void init() throws ServletException {
universe@36 127 moduleInfo = Optional.ofNullable(this.getClass().getAnnotation(LightPITModule.class))
universe@36 128 .map(LightPITModule.ELProxy::new).orElse(null);
universe@33 129
universe@33 130 if (moduleInfo != null) {
universe@12 131 scanForRequestMappings();
universe@12 132 }
universe@33 133
universe@12 134 LOG.trace("{} initialized", getServletName());
universe@12 135 }
universe@12 136
universe@12 137 private void scanForRequestMappings() {
universe@12 138 try {
universe@11 139 Method[] methods = getClass().getDeclaredMethods();
universe@11 140 for (Method method : methods) {
universe@11 141 Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
universe@11 142 if (mapping.isPresent()) {
universe@11 143 if (!Modifier.isPublic(method.getModifiers())) {
universe@11 144 LOG.warn("{} is annotated with {} but is not public",
universe@11 145 method.getName(), RequestMapping.class.getSimpleName()
universe@11 146 );
universe@11 147 continue;
universe@11 148 }
universe@11 149 if (Modifier.isAbstract(method.getModifiers())) {
universe@11 150 LOG.warn("{} is annotated with {} but is abstract",
universe@11 151 method.getName(), RequestMapping.class.getSimpleName()
universe@11 152 );
universe@11 153 continue;
universe@11 154 }
universe@12 155 if (!ResponseType.class.isAssignableFrom(method.getReturnType())) {
universe@12 156 LOG.warn("{} is annotated with {} but has the wrong return type - 'ResponseType' required",
universe@12 157 method.getName(), RequestMapping.class.getSimpleName()
universe@12 158 );
universe@12 159 continue;
universe@12 160 }
universe@12 161
universe@42 162 boolean paramsInjectible = true;
universe@42 163 for (var param : method.getParameterTypes()) {
universe@42 164 paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param)
universe@42 165 || HttpServletResponse.class.isAssignableFrom(param)
universe@42 166 || DataAccessObjects.class.isAssignableFrom(param);
universe@42 167 }
universe@42 168 if (paramsInjectible) {
universe@34 169 final String requestPath = "/" + mapping.get().requestPath();
universe@12 170
universe@39 171 if (mappings
universe@39 172 .computeIfAbsent(mapping.get().method(), k -> new HashMap<>())
universe@39 173 .putIfAbsent(requestPath, method) != null) {
universe@11 174 LOG.warn("{} {} has multiple mappings",
universe@11 175 mapping.get().method(),
universe@11 176 mapping.get().requestPath()
universe@11 177 );
universe@11 178 }
universe@12 179
universe@45 180 final var menuKey = mapping.get().menuKey();
universe@45 181 if (!menuKey.isBlank()) {
universe@45 182 subMenu.add(new MenuEntry(
universe@45 183 new ResourceKey(moduleInfo.getBundleBaseName(), menuKey),
universe@45 184 moduleInfo.getModulePath() + requestPath,
universe@45 185 mapping.get().menuSequence()));
universe@45 186 }
universe@45 187
universe@22 188 LOG.debug("{} {} maps to {}::{}",
universe@11 189 mapping.get().method(),
universe@18 190 requestPath,
universe@22 191 getClass().getSimpleName(),
universe@11 192 method.getName()
universe@11 193 );
universe@11 194 } else {
universe@42 195 LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest. HttpServletResponse, and DataAccessObjects are allowed",
universe@11 196 method.getName(), RequestMapping.class.getSimpleName()
universe@11 197 );
universe@11 198 }
universe@11 199 }
universe@11 200 }
universe@12 201 } catch (SecurityException ex) {
universe@12 202 LOG.error("Scan for request mappings on declared methods failed.", ex);
universe@11 203 }
universe@11 204 }
universe@11 205
universe@11 206 @Override
universe@11 207 public void destroy() {
universe@11 208 mappings.clear();
universe@11 209 LOG.trace("{} destroyed", getServletName());
universe@11 210 }
universe@34 211
universe@13 212 /**
universe@13 213 * Sets the name of the dynamic fragment.
universe@34 214 * <p>
universe@13 215 * It is sufficient to specify the name without any extension. The extension
universe@13 216 * is added automatically if not specified.
universe@34 217 * <p>
universe@13 218 * The fragment must be located in the dynamic fragments folder.
universe@34 219 *
universe@34 220 * @param req the servlet request object
universe@13 221 * @param fragmentName the name of the fragment
universe@13 222 * @see Constants#DYN_FRAGMENT_PATH_PREFIX
universe@13 223 */
universe@13 224 public void setDynamicFragment(HttpServletRequest req, String fragmentName) {
universe@13 225 req.setAttribute(Constants.REQ_ATTR_FRAGMENT, Functions.dynFragmentPath(fragmentName));
universe@13 226 }
universe@34 227
universe@11 228 /**
universe@47 229 * @param req the servlet request object
universe@47 230 * @param location the location where to redirect
universe@47 231 * @see Constants#REQ_ATTR_REDIRECT_LOCATION
universe@47 232 */
universe@47 233 public void setRedirectLocation(HttpServletRequest req, String location) {
universe@47 234 if (location.startsWith("./")) {
universe@47 235 location = location.replaceFirst("\\./", Functions.baseHref(req));
universe@47 236 }
universe@47 237 req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location);
universe@47 238 }
universe@47 239
universe@47 240 /**
universe@13 241 * Specifies the name of an additional stylesheet used by the module.
universe@34 242 * <p>
universe@13 243 * Setting an additional stylesheet is optional, but quite common for HTML
universe@13 244 * output.
universe@34 245 * <p>
universe@13 246 * It is sufficient to specify the name without any extension. The extension
universe@13 247 * is added automatically if not specified.
universe@34 248 *
universe@34 249 * @param req the servlet request object
universe@13 250 * @param stylesheet the name of the stylesheet
universe@11 251 */
universe@13 252 public void setStylesheet(HttpServletRequest req, String stylesheet) {
universe@13 253 req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Functions.enforceExt(stylesheet, ".css"));
universe@10 254 }
universe@34 255
universe@47 256 /**
universe@47 257 * Obtains a request parameter of the specified type.
universe@47 258 * The specified type must have a single-argument constructor accepting a string to perform conversion.
universe@47 259 * The constructor of the specified type may throw an exception on conversion failures.
universe@47 260 *
universe@47 261 * @param req the servlet request object
universe@47 262 * @param clazz the class object of the expected type
universe@47 263 * @param name the name of the parameter
universe@47 264 * @param <T> the expected type
universe@47 265 * @return the parameter value or an empty optional, if no parameter with the specified name was found
universe@47 266 */
universe@47 267 public<T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) {
universe@47 268 final String paramValue = req.getParameter(name);
universe@47 269 if (paramValue == null) return Optional.empty();
universe@47 270 if (clazz.equals(String.class)) return Optional.of((T)paramValue);
universe@47 271 try {
universe@47 272 final Constructor<T> ctor = clazz.getConstructor(String.class);
universe@47 273 return Optional.of(ctor.newInstance(paramValue));
universe@47 274 } catch (ReflectiveOperationException e) {
universe@47 275 throw new RuntimeException(e);
universe@47 276 }
universe@47 277
universe@47 278 }
universe@47 279
universe@10 280 private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp)
universe@10 281 throws IOException, ServletException {
universe@34 282
universe@36 283 req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu());
universe@45 284 req.setAttribute(Constants.REQ_ATTR_SUB_MENU, subMenu);
universe@43 285 req.getRequestDispatcher(SITE_JSP).forward(req, resp);
universe@10 286 }
universe@34 287
universe@45 288 private String sanitizeRequestPath(HttpServletRequest req) {
universe@45 289 return Optional.ofNullable(req.getPathInfo()).orElse("/");
universe@45 290 }
universe@45 291
universe@39 292 private Optional<Method> findMapping(HttpMethod method, HttpServletRequest req) {
universe@45 293 return Optional.ofNullable(mappings.get(method)).map(rm -> rm.get(sanitizeRequestPath(req)));
universe@11 294 }
universe@34 295
universe@34 296 private void forwardAsSpecified(ResponseType type, HttpServletRequest req, HttpServletResponse resp)
universe@12 297 throws ServletException, IOException {
universe@12 298 switch (type) {
universe@34 299 case NONE:
universe@34 300 return;
universe@43 301 case HTML:
universe@12 302 forwardToFullView(req, resp);
universe@12 303 return;
universe@12 304 // TODO: implement remaining response types
universe@12 305 default:
universe@34 306 throw new AssertionError("ResponseType switch is not exhaustive - this is a bug!");
universe@12 307 }
universe@12 308 }
universe@34 309
universe@38 310 private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
universe@27 311
universe@13 312 // choose the requested language as session language (if available) or fall back to english, otherwise
universe@20 313 HttpSession session = req.getSession();
universe@13 314 if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
universe@13 315 Optional<List<String>> availableLanguages = Functions.availableLanguages(getServletContext()).map(Arrays::asList);
universe@13 316 Optional<Locale> reqLocale = Optional.of(req.getLocale());
universe@13 317 Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH);
universe@13 318 session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale);
universe@34 319 LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage());
universe@14 320 } else {
universe@15 321 Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE);
universe@15 322 resp.setLocale(sessionLocale);
universe@15 323 LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale);
universe@13 324 }
universe@34 325
universe@21 326 // set some internal request attributes
universe@53 327 final String fullPath = Functions.fullPath(req);
universe@47 328 req.setAttribute(Constants.REQ_ATTR_BASE_HREF, Functions.baseHref(req));
universe@53 329 req.setAttribute(Constants.REQ_ATTR_PATH, fullPath);
universe@36 330 Optional.ofNullable(moduleInfo).ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
universe@34 331
universe@53 332 // if this is an error path, bypass the normal flow
universe@53 333 if (fullPath.startsWith("/error/")) {
universe@53 334 final var mapping = findMapping(method, req);
universe@53 335 if (mapping.isPresent()) {
universe@53 336 forwardAsSpecified(invokeMapping(mapping.get(), req, resp, null), req, resp);
universe@53 337 }
universe@53 338 return;
universe@53 339 }
universe@53 340
universe@38 341 // obtain a connection and create the data access objects
universe@38 342 final var db = (DatabaseFacade) req.getServletContext().getAttribute(DatabaseFacade.SC_ATTR_NAME);
universe@53 343 final var ds = db.getDataSource();
universe@53 344 if (ds == null) {
universe@53 345 resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details.");
universe@53 346 return;
universe@53 347 }
universe@53 348 try (final var connection = ds.getConnection()) {
universe@38 349 final var dao = createDataAccessObjects(connection);
universe@39 350 try {
universe@39 351 connection.setAutoCommit(false);
universe@39 352 // call the handler, if available, or send an HTTP 404 error
universe@39 353 final var mapping = findMapping(method, req);
universe@39 354 if (mapping.isPresent()) {
universe@39 355 forwardAsSpecified(invokeMapping(mapping.get(), req, resp, dao), req, resp);
universe@39 356 } else {
universe@39 357 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
universe@39 358 }
universe@39 359 connection.commit();
universe@39 360 } catch (SQLException ex) {
universe@39 361 LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage());
universe@39 362 LOG.debug("Details: ", ex);
universe@39 363 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code:" + ex.getErrorCode());
universe@39 364 connection.rollback();
universe@38 365 }
universe@38 366 } catch (SQLException ex) {
universe@39 367 LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage());
universe@38 368 LOG.debug("Details: ", ex);
universe@38 369 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code:" + ex.getErrorCode());
universe@12 370 }
universe@12 371 }
universe@34 372
universe@7 373 @Override
universe@7 374 protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
universe@7 375 throws ServletException, IOException {
universe@12 376 doProcess(HttpMethod.GET, req, resp);
universe@7 377 }
universe@7 378
universe@7 379 @Override
universe@7 380 protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
universe@7 381 throws ServletException, IOException {
universe@12 382 doProcess(HttpMethod.POST, req, resp);
universe@7 383 }
universe@7 384 }

mercurial