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

Wed, 13 May 2020 18:31:52 +0200

author
Mike Becker <universe@uap-core.de>
date
Wed, 13 May 2020 18:31:52 +0200
changeset 40
276ef00a336d
parent 39
e722861558bb
child 42
f962ff9dd44e
permissions
-rw-r--r--

fixes handler signatures and one compile error for AbstractDao.setForeignKeyOrNull()

     1 /*
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright 2018 Mike Becker. All rights reserved.
     5  *
     6  * Redistribution and use in source and binary forms, with or without
     7  * modification, are permitted provided that the following conditions are met:
     8  *
     9  *   1. Redistributions of source code must retain the above copyright
    10  *      notice, this list of conditions and the following disclaimer.
    11  *
    12  *   2. Redistributions in binary form must reproduce the above copyright
    13  *      notice, this list of conditions and the following disclaimer in the
    14  *      documentation and/or other materials provided with the distribution.
    15  *
    16  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    17  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    18  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    19  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    20  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    21  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    22  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    23  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    24  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    25  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    26  * POSSIBILITY OF SUCH DAMAGE.
    27  *
    28  */
    29 package de.uapcore.lightpit;
    31 import de.uapcore.lightpit.dao.DataAccessObjects;
    32 import de.uapcore.lightpit.dao.postgres.PGDataAccessObjects;
    33 import org.slf4j.Logger;
    34 import org.slf4j.LoggerFactory;
    36 import javax.servlet.ServletException;
    37 import javax.servlet.http.HttpServlet;
    38 import javax.servlet.http.HttpServletRequest;
    39 import javax.servlet.http.HttpServletResponse;
    40 import javax.servlet.http.HttpSession;
    41 import java.io.IOException;
    42 import java.lang.reflect.Method;
    43 import java.lang.reflect.Modifier;
    44 import java.sql.Connection;
    45 import java.sql.SQLException;
    46 import java.util.*;
    48 /**
    49  * A special implementation of a HTTPServlet which is focused on implementing
    50  * the necessary functionality for {@link LightPITModule}s.
    51  */
    52 public abstract class AbstractLightPITServlet extends HttpServlet {
    54     private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class);
    56     private static final String HTML_FULL_DISPATCHER = Functions.jspPath("html_full");
    58     /**
    59      * The EL proxy is necessary, because the EL resolver cannot handle annotation properties.
    60      */
    61     private LightPITModule.ELProxy moduleInfo = null;
    63     /**
    64      * Invocation mapping gathered from the {@link RequestMapping} annotations.
    65      * <p>
    66      * Paths in this map must always start with a leading slash, although
    67      * the specification in the annotation must not start with a leading slash.
    68      * <p>
    69      * The reason for this is the different handling of empty paths in
    70      * {@link HttpServletRequest#getPathInfo()}.
    71      */
    72     private final Map<HttpMethod, Map<String, Method>> mappings = new HashMap<>();
    74     /**
    75      * Gives implementing modules access to the {@link ModuleManager}.
    76      *
    77      * @return the module manager
    78      */
    79     protected final ModuleManager getModuleManager() {
    80         return (ModuleManager) getServletContext().getAttribute(ModuleManager.SC_ATTR_NAME);
    81     }
    84     /**
    85      * Creates a set of data access objects for the specified connection.
    86      *
    87      * @param connection the SQL connection
    88      * @return a set of data access objects
    89      */
    90     private DataAccessObjects createDataAccessObjects(Connection connection) throws SQLException {
    91         final var df = (DatabaseFacade) getServletContext().getAttribute(DatabaseFacade.SC_ATTR_NAME);
    92         if (df.getSQLDialect() == DatabaseFacade.Dialect.Postgres) {
    93             return new PGDataAccessObjects(connection);
    94         }
    95         throw new AssertionError("Non-exhaustive if-else - this is a bug.");
    96     }
    98     private ResponseType invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
    99         try {
   100             LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName());
   101             return (ResponseType) method.invoke(this, req, resp, dao);
   102         } catch (ReflectiveOperationException | ClassCastException ex) {
   103             LOG.error("invocation of method {} failed: {}", method.getName(), ex.getMessage());
   104             LOG.debug("Details: ", ex);
   105             resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
   106             return ResponseType.NONE;
   107         }
   108     }
   110     @Override
   111     public void init() throws ServletException {
   112         moduleInfo = Optional.ofNullable(this.getClass().getAnnotation(LightPITModule.class))
   113                 .map(LightPITModule.ELProxy::new).orElse(null);
   115         if (moduleInfo != null) {
   116             scanForRequestMappings();
   117         }
   119         LOG.trace("{} initialized", getServletName());
   120     }
   122     private void scanForRequestMappings() {
   123         try {
   124             Method[] methods = getClass().getDeclaredMethods();
   125             for (Method method : methods) {
   126                 Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
   127                 if (mapping.isPresent()) {
   128                     if (!Modifier.isPublic(method.getModifiers())) {
   129                         LOG.warn("{} is annotated with {} but is not public",
   130                                 method.getName(), RequestMapping.class.getSimpleName()
   131                         );
   132                         continue;
   133                     }
   134                     if (Modifier.isAbstract(method.getModifiers())) {
   135                         LOG.warn("{} is annotated with {} but is abstract",
   136                                 method.getName(), RequestMapping.class.getSimpleName()
   137                         );
   138                         continue;
   139                     }
   140                     if (!ResponseType.class.isAssignableFrom(method.getReturnType())) {
   141                         LOG.warn("{} is annotated with {} but has the wrong return type - 'ResponseType' required",
   142                                 method.getName(), RequestMapping.class.getSimpleName()
   143                         );
   144                         continue;
   145                     }
   147                     Class<?>[] params = method.getParameterTypes();
   148                     if (params.length == 3
   149                             && HttpServletRequest.class.isAssignableFrom(params[0])
   150                             && HttpServletResponse.class.isAssignableFrom(params[1])
   151                             && DataAccessObjects.class.isAssignableFrom(params[2])) {
   153                         final String requestPath = "/" + mapping.get().requestPath();
   155                         if (mappings
   156                                 .computeIfAbsent(mapping.get().method(), k -> new HashMap<>())
   157                                 .putIfAbsent(requestPath, method) != null) {
   158                             LOG.warn("{} {} has multiple mappings",
   159                                     mapping.get().method(),
   160                                     mapping.get().requestPath()
   161                             );
   162                         }
   164                         LOG.debug("{} {} maps to {}::{}",
   165                                 mapping.get().method(),
   166                                 requestPath,
   167                                 getClass().getSimpleName(),
   168                                 method.getName()
   169                         );
   170                     } else {
   171                         LOG.warn("{} is annotated with {} but has the wrong parameters - (HttpServletRequest,HttpServletResponse,DataAccessObjects) required",
   172                                 method.getName(), RequestMapping.class.getSimpleName()
   173                         );
   174                     }
   175                 }
   176             }
   177         } catch (SecurityException ex) {
   178             LOG.error("Scan for request mappings on declared methods failed.", ex);
   179         }
   180     }
   182     @Override
   183     public void destroy() {
   184         mappings.clear();
   185         LOG.trace("{} destroyed", getServletName());
   186     }
   188     /**
   189      * Sets the name of the dynamic fragment.
   190      * <p>
   191      * It is sufficient to specify the name without any extension. The extension
   192      * is added automatically if not specified.
   193      * <p>
   194      * The fragment must be located in the dynamic fragments folder.
   195      *
   196      * @param req          the servlet request object
   197      * @param fragmentName the name of the fragment
   198      * @see Constants#DYN_FRAGMENT_PATH_PREFIX
   199      */
   200     public void setDynamicFragment(HttpServletRequest req, String fragmentName) {
   201         req.setAttribute(Constants.REQ_ATTR_FRAGMENT, Functions.dynFragmentPath(fragmentName));
   202     }
   204     /**
   205      * Specifies the name of an additional stylesheet used by the module.
   206      * <p>
   207      * Setting an additional stylesheet is optional, but quite common for HTML
   208      * output.
   209      * <p>
   210      * It is sufficient to specify the name without any extension. The extension
   211      * is added automatically if not specified.
   212      *
   213      * @param req        the servlet request object
   214      * @param stylesheet the name of the stylesheet
   215      */
   216     public void setStylesheet(HttpServletRequest req, String stylesheet) {
   217         req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Functions.enforceExt(stylesheet, ".css"));
   218     }
   220     private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp)
   221             throws IOException, ServletException {
   223         req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu());
   224         req.getRequestDispatcher(HTML_FULL_DISPATCHER).forward(req, resp);
   225     }
   227     private Optional<Method> findMapping(HttpMethod method, HttpServletRequest req) {
   228         return Optional.ofNullable(mappings.get(method))
   229                 .map(rm -> rm.get(Optional.ofNullable(req.getPathInfo()).orElse("/"))
   230                 );
   231     }
   233     private void forwardAsSpecified(ResponseType type, HttpServletRequest req, HttpServletResponse resp)
   234             throws ServletException, IOException {
   235         switch (type) {
   236             case NONE:
   237                 return;
   238             case HTML_FULL:
   239                 forwardToFullView(req, resp);
   240                 return;
   241             // TODO: implement remaining response types
   242             default:
   243                 throw new AssertionError("ResponseType switch is not exhaustive - this is a bug!");
   244         }
   245     }
   247     private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
   249         // choose the requested language as session language (if available) or fall back to english, otherwise
   250         HttpSession session = req.getSession();
   251         if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
   252             Optional<List<String>> availableLanguages = Functions.availableLanguages(getServletContext()).map(Arrays::asList);
   253             Optional<Locale> reqLocale = Optional.of(req.getLocale());
   254             Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH);
   255             session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale);
   256             LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage());
   257         } else {
   258             Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE);
   259             resp.setLocale(sessionLocale);
   260             LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale);
   261         }
   263         // set some internal request attributes
   264         req.setAttribute(Constants.REQ_ATTR_PATH, Functions.fullPath(req));
   265         req.setAttribute(Constants.REQ_ATTR_MODULE_CLASSNAME, this.getClass().getName());
   266         Optional.ofNullable(moduleInfo).ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
   268         // obtain a connection and create the data access objects
   269         final var db = (DatabaseFacade) req.getServletContext().getAttribute(DatabaseFacade.SC_ATTR_NAME);
   270         try (final var connection = db.getDataSource().getConnection()) {
   271             final var dao = createDataAccessObjects(connection);
   272             try {
   273                 connection.setAutoCommit(false);
   274                 // call the handler, if available, or send an HTTP 404 error
   275                 final var mapping = findMapping(method, req);
   276                 if (mapping.isPresent()) {
   277                     forwardAsSpecified(invokeMapping(mapping.get(), req, resp, dao), req, resp);
   278                 } else {
   279                     resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   280                 }
   281                 connection.commit();
   282             } catch (SQLException ex) {
   283                 LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage());
   284                 LOG.debug("Details: ", ex);
   285                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code:" + ex.getErrorCode());
   286                 connection.rollback();
   287             }
   288         } catch (SQLException ex) {
   289             LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage());
   290             LOG.debug("Details: ", ex);
   291             resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code:" + ex.getErrorCode());
   292         }
   293     }
   295     @Override
   296     protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
   297             throws ServletException, IOException {
   298         doProcess(HttpMethod.GET, req, resp);
   299     }
   301     @Override
   302     protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
   303             throws ServletException, IOException {
   304         doProcess(HttpMethod.POST, req, resp);
   305     }
   306 }

mercurial