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

Sun, 01 Apr 2018 18:16:47 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 01 Apr 2018 18:16:47 +0200
changeset 21
b213fef2539e
parent 20
bd1a76c91d5b
child 22
5a91fb7067af
permissions
-rw-r--r--

adds first part of a module manager UI

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2017 Mike Becker. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 */
package de.uapcore.lightpit;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A special implementation of a HTTPServlet which is focused on implementing
 * the necessary functionality for {@link LightPITModule}s.
 */
public abstract class AbstractLightPITServlet extends HttpServlet {
    
    private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class);
    
    private static final String HTML_FULL_DISPATCHER = Functions.jspPath("html_full");
    
    /**
     * Store a reference to the annotation for quicker access.
     */
    private Optional<LightPITModule> moduleInfo = Optional.empty();

    /**
     * The EL proxy is necessary, because the EL resolver cannot handle annotation properties.
     */
    private Optional<LightPITModule.ELProxy> moduleInfoELProxy = Optional.empty();
    
    
    @FunctionalInterface
    private static interface HandlerMethod {
        ResponseType apply(HttpServletRequest t, HttpServletResponse u) throws IOException, ServletException;
    }
    
    /**
     * Invocation mapping gathered from the {@link RequestMapping} annotations.
     * 
     * Paths in this map must always start with a leading slash, although
     * the specification in the annotation must not start with a leading slash.
     * 
     * The reason for this is the different handling of empty paths in 
     * {@link HttpServletRequest#getPathInfo()}.
     */
    private final Map<HttpMethod, Map<String, HandlerMethod>> mappings = new HashMap<>();

    /**
     * Gives implementing modules access to the {@link ModuleManager}.
     * @return the module manager
     */
    protected final ModuleManager getModuleManager() {
        return (ModuleManager) getServletContext().getAttribute(ModuleManager.SC_ATTR_NAME);
    }
    
    /**
     * Gives implementing modules access to the {@link DatabaseFacade}.
     * @return the database facade
     */
    protected final DatabaseFacade getDatabaseFacade() {
        return (DatabaseFacade) getServletContext().getAttribute(DatabaseFacade.SC_ATTR_NAME);
    }
    
    private ResponseType invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {
        try {
            LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName());
            return (ResponseType) method.invoke(this, req, resp);
        } catch (ReflectiveOperationException | ClassCastException ex) {
            LOG.error(String.format("invocation of method %s failed", method.getName()), ex);
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            return ResponseType.NONE;
        }
    }

    @Override
    public void init() throws ServletException {
        moduleInfo = Optional.ofNullable(this.getClass().getAnnotation(LightPITModule.class));
        moduleInfoELProxy = moduleInfo.map(LightPITModule.ELProxy::convert);
        
        if (moduleInfo.isPresent()) {
            scanForRequestMappings();
        }
        
        LOG.trace("{} initialized", getServletName());
    }

    private void scanForRequestMappings() {
        try {
            Method[] methods = getClass().getDeclaredMethods();
            for (Method method : methods) {
                Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
                if (mapping.isPresent()) {
                    if (!Modifier.isPublic(method.getModifiers())) {
                        LOG.warn("{} is annotated with {} but is not public",
                                method.getName(), RequestMapping.class.getSimpleName()
                        );
                        continue;
                    }
                    if (Modifier.isAbstract(method.getModifiers())) {
                        LOG.warn("{} is annotated with {} but is abstract",
                                method.getName(), RequestMapping.class.getSimpleName()
                        );
                        continue;
                    }
                    if (!ResponseType.class.isAssignableFrom(method.getReturnType())) {
                        LOG.warn("{} is annotated with {} but has the wrong return type - 'ResponseType' required",
                                method.getName(), RequestMapping.class.getSimpleName()
                        );
                        continue;
                    }

                    Class<?>[] params = method.getParameterTypes();
                    if (params.length == 2
                            && HttpServletRequest.class.isAssignableFrom(params[0])
                            && HttpServletResponse.class.isAssignableFrom(params[1])) {
                        
                        final String requestPath = "/"+mapping.get().requestPath();

                        if (mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>()).
                                putIfAbsent(requestPath,
                                        (req, resp) -> invokeMapping(method, req, resp)) != null) {
                            LOG.warn("{} {} has multiple mappings",
                                    mapping.get().method(),
                                    mapping.get().requestPath()
                            );
                        }

                        LOG.info("{} {} maps to {}",
                                mapping.get().method(),
                                requestPath,
                                method.getName()
                        );
                    } else {
                        LOG.warn("{} is annotated with {} but has the wrong parameters - (HttpServletRequest,HttpServletResponse) required",
                                method.getName(), RequestMapping.class.getSimpleName()
                        );
                    }
                }
            }
        } catch (SecurityException ex) {
            LOG.error("Scan for request mappings on declared methods failed.", ex);
        }
    }

    @Override
    public void destroy() {
        mappings.clear();
        LOG.trace("{} destroyed", getServletName());
    }
    
    /**
     * Sets the name of the dynamic fragment.
     * 
     * It is sufficient to specify the name without any extension. The extension
     * is added automatically if not specified.
     * 
     * The fragment must be located in the dynamic fragments folder.
     * 
     * @param req the servlet request object
     * @param fragmentName the name of the fragment
     * @see Constants#DYN_FRAGMENT_PATH_PREFIX
     */
    public void setDynamicFragment(HttpServletRequest req, String fragmentName) {
        req.setAttribute(Constants.REQ_ATTR_FRAGMENT, Functions.dynFragmentPath(fragmentName));
    }
    
    /**
     * Specifies the name of an additional stylesheet used by the module.
     * 
     * Setting an additional stylesheet is optional, but quite common for HTML
     * output.
     * 
     * It is sufficient to specify the name without any extension. The extension
     * is added automatically if not specified.
     * 
     * @param req the servlet request object
     * @param stylesheet the name of the stylesheet
     */
    public void setStylesheet(HttpServletRequest req, String stylesheet) {
        req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Functions.enforceExt(stylesheet, ".css"));
    }
    
    private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {
        
        req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu());
        req.getRequestDispatcher(HTML_FULL_DISPATCHER).forward(req, resp);
    }
    
    private Optional<HandlerMethod> findMapping(HttpMethod method, HttpServletRequest req) {
        return Optional.ofNullable(mappings.get(method)).map(
                (rm) -> rm.get(Optional.ofNullable(req.getPathInfo()).orElse("/"))
        );
    }
    
    private void forwardAsSepcified(ResponseType type, HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        switch (type) {
            case NONE: return;
            case HTML_FULL:
                forwardToFullView(req, resp);
                return;
            // TODO: implement remaining response types
            default:
                // this code should be unreachable
                LOG.error("ResponseType switch is not exhaustive - this is a bug!");
                throw new UnsupportedOperationException();
        }
    }
    
    private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        
        // Synchronize module information with database
        getModuleManager().syncWithDatabase(getDatabaseFacade());
        
        // choose the requested language as session language (if available) or fall back to english, otherwise
        HttpSession session = req.getSession();
        if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
            Optional<List<String>> availableLanguages = Functions.availableLanguages(getServletContext()).map(Arrays::asList);
            Optional<Locale> reqLocale = Optional.of(req.getLocale());
            Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH);
            session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale);
            LOG.debug("Settng language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage());
        } else {
            Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE);
            resp.setLocale(sessionLocale);
            LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale);
        }
        
        // set some internal request attributes
        req.setAttribute(Constants.REQ_ATTR_PATH, Functions.fullPath(req));
        req.setAttribute(Constants.REQ_ATTR_MODULE_CLASSNAME, this.getClass().getName());
        moduleInfoELProxy.ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
        
        
        // call the handler, if available, or send an HTTP 404 error
        Optional<HandlerMethod> mapping = findMapping(method, req);
        if (mapping.isPresent()) {
            forwardAsSepcified(mapping.get().apply(req, resp), req, resp);
        } else {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
    
    @Override
    protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doProcess(HttpMethod.GET, req, resp);
    }

    @Override
    protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doProcess(HttpMethod.POST, req, resp);
    }
}

mercurial