Tue, 26 Dec 2017 17:36:47 +0100
adds dynamic fragments to LightPIT request handling framework + basic language recognition code
--- a/src/java/de/uapcore/lightpit/AbstractLightPITServlet.java Sat Dec 23 17:28:19 2017 +0100 +++ b/src/java/de/uapcore/lightpit/AbstractLightPITServlet.java Tue Dec 26 17:36:47 2017 +0100 @@ -31,13 +31,17 @@ 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; @@ -49,6 +53,8 @@ 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. */ @@ -164,29 +170,43 @@ LOG.trace("{} destroyed", getServletName()); } - /** - * Sets several requests attributes, that can be used by the JSP. + * 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 - * @see Constants#REQ_ATTR_PATH - * @see Constants#REQ_ATTR_MODULE_CLASSNAME - * @see Constants#REQ_ATTR_MODULE_INFO + * @param fragmentName the name of the fragment + * @see Constants#DYN_FRAGMENT_PATH_PREFIX */ - private void setGenericRequestAttributes(HttpServletRequest req) { - 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)); + 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 { - setGenericRequestAttributes(req); req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu()); - req.getRequestDispatcher(Functions.jspPath("full.jsp")).forward(req, resp); + req.getRequestDispatcher(HTML_FULL_DISPATCHER).forward(req, resp); } private Optional<HandlerMethod> findMapping(HttpMethod method, HttpServletRequest req) { @@ -212,6 +232,22 @@ private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + HttpSession session = req.getSession(); + + // choose the requested language as session language (if available) or fall back to english, otherwise + 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()); + } + + 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)); + Optional<HandlerMethod> mapping = findMapping(method, req); if (mapping.isPresent()) { forwardAsSepcified(mapping.get().apply(req, resp), req, resp);
--- a/src/java/de/uapcore/lightpit/Constants.java Sat Dec 23 17:28:19 2017 +0100 +++ b/src/java/de/uapcore/lightpit/Constants.java Tue Dec 26 17:36:47 2017 +0100 @@ -31,11 +31,23 @@ import static de.uapcore.lightpit.Functions.fqn; /** - * Contains all constants used by the this application. + * Contains all non-local scope constants used by the this application. + * + * Constants with (class) local scope are defined in their respective classes. */ public final class Constants { public static final String JSP_PATH_PREFIX = "/WEB-INF/jsp/"; + public static final String JSPF_PATH_PREFIX = "/WEB-INF/jspf/"; + + public static final String DYN_FRAGMENT_PATH_PREFIX = "/WEB-INF/dynamic_fragments/"; + + + /** + * Name for the context parameter specifying the available languages. + */ + public static final String CTX_ATTR_LANGUAGES = "available-languages"; + /** * Key for the request attribute containing the class name of the currently dispatching module. */ @@ -55,6 +67,22 @@ * Key for the request attribute containing the full path information (servlet path + path info). */ public static final String REQ_ATTR_PATH = fqn(AbstractLightPITServlet.class, "path"); + + /** + * Key for the name of the fragment which should be rendered. + */ + public static final String REQ_ATTR_FRAGMENT = fqn(AbstractLightPITServlet.class, "fragment"); + + /** + * Key for the name of the additional stylesheet used by a module. + */ + public static final String REQ_ATTR_STYLESHEET = fqn(AbstractLightPITServlet.class, "extraCss"); + + + /** + * Key for the current language selection within the session. + */ + public static final String SESSION_ATTR_LANGUAGE = fqn(AbstractLightPITServlet.class, "language"); /** * This class is not instantiatable.
--- a/src/java/de/uapcore/lightpit/Functions.java Sat Dec 23 17:28:19 2017 +0100 +++ b/src/java/de/uapcore/lightpit/Functions.java Tue Dec 26 17:36:47 2017 +0100 @@ -29,6 +29,7 @@ package de.uapcore.lightpit; import java.util.Optional; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,8 +41,24 @@ private static final Logger LOG = LoggerFactory.getLogger(Functions.class); + public static Optional<String[]> availableLanguages(ServletContext ctx) { + return Optional.ofNullable(ctx.getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*")); + } + + public static String enforceExt(String filename, String ext) { + return filename.endsWith(ext) ? filename : filename + ext; + } + public static String jspPath(String filename) { - return Constants.JSP_PATH_PREFIX + filename; + return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp"); + } + + public static String jspfPath(String filename) { + return enforceExt(Constants.JSPF_PATH_PREFIX + filename, ".jspf"); + } + + public static String dynFragmentPath(String filename) { + return enforceExt(Constants.DYN_FRAGMENT_PATH_PREFIX + filename, ".jsp"); } public static String fqn(String base, String name) {
--- a/src/java/de/uapcore/lightpit/modules/LanguageModule.java Sat Dec 23 17:28:19 2017 +0100 +++ b/src/java/de/uapcore/lightpit/modules/LanguageModule.java Tue Dec 26 17:36:47 2017 +0100 @@ -30,12 +30,22 @@ import de.uapcore.lightpit.LightPITModule; import de.uapcore.lightpit.AbstractLightPITServlet; +import de.uapcore.lightpit.Constants; +import de.uapcore.lightpit.Functions; import de.uapcore.lightpit.HttpMethod; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import de.uapcore.lightpit.RequestMapping; import de.uapcore.lightpit.ResponseType; +import java.util.ArrayList; +import java.util.IllformedLocaleException; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import javax.servlet.ServletException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @LightPITModule( @@ -48,9 +58,42 @@ ) public final class LanguageModule extends AbstractLightPITServlet { + private static final Logger LOG = LoggerFactory.getLogger(LanguageModule.class); + + private List<Locale> languages = new ArrayList<>(); + + @Override + public void init() throws ServletException { + super.init(); + + Optional<String[]> langs = Functions.availableLanguages(getServletContext()); + if (langs.isPresent()) { + for (String lang : langs.get()) { + try { + Locale locale = Locale.forLanguageTag(lang); + if (locale.getLanguage().isEmpty()) { + throw new IllformedLocaleException(); + } + languages.add(locale); + } catch (IllformedLocaleException ex) { + LOG.warn("Specified lanaguge {} in context parameter cannot be mapped to an existing locale - skipping.", lang); + } + } + + } else { + languages.add(Locale.ENGLISH); + LOG.warn("Context parameter 'available-languges' not found. Only english will be available."); + } + } + @RequestMapping(method = HttpMethod.GET) public ResponseType handle(HttpServletRequest req, HttpServletResponse resp) { + + req.setAttribute("languages", languages); + req.setAttribute("browserLanguage", req.getLocale()); + setStylesheet(req, "language"); + setDynamicFragment(req, "language"); return ResponseType.HTML_FULL; } }
--- a/src/java/de/uapcore/lightpit/resources/localization/language.properties Sat Dec 23 17:28:19 2017 +0100 +++ b/src/java/de/uapcore/lightpit/resources/localization/language.properties Tue Dec 26 17:36:47 2017 +0100 @@ -22,3 +22,5 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. menuLabel = Languages +submit = Switch language +browserLanguage = Browser language
--- a/src/java/de/uapcore/lightpit/resources/localization/language_de.properties Sat Dec 23 17:28:19 2017 +0100 +++ b/src/java/de/uapcore/lightpit/resources/localization/language_de.properties Tue Dec 26 17:36:47 2017 +0100 @@ -22,3 +22,5 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. menuLabel = Sprache +submit = Sprache ausw\u00e4hlen +browserLanguage = Browsersprache
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java/logging.properties Tue Dec 26 17:36:47 2017 +0100 @@ -0,0 +1,27 @@ +# 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. + + +handlers= java.util.logging.ConsoleHandler + +.level = DEBUG
--- a/web/META-INF/context.xml Sat Dec 23 17:28:19 2017 +0100 +++ b/web/META-INF/context.xml Tue Dec 26 17:36:47 2017 +0100 @@ -1,2 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> -<Context path="/lightpit"/> +<Context path="/lightpit" />
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/WEB-INF/dynamic_fragments/language.jsp Tue Dec 26 17:36:47 2017 +0100 @@ -0,0 +1,46 @@ +<%-- +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. +--%> +<%@page pageEncoding="UTF-8" session="true" %> +<%@page import="de.uapcore.lightpit.Constants" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +<c:set scope="page" var="currentLanguage" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}" /> + +<fmt:bundle basename="${requestScope[Constants.REQ_ATTR_MODULE_INFO].bundleBaseName}"> +<form method="POST"id="lang-selector"> + <c:forEach items="${languages}" var="l"> + <label> + <input type="radio" name="language" value="${l.language}" + <c:if test="${l.language eq currentLanguage.language}">checked</c:if>/> + ${l.displayLanguage} + (${l.getDisplayLanguage(currentLanguage)}<c:if test="${not empty browserLanguage and l.language eq browserLanguage.language}"> - <fmt:message key="browserLanguage"/></c:if>) + </label> + </c:forEach> + <input type="submit" value="<fmt:message key="submit" />"/> +</form> +</fmt:bundle>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/WEB-INF/jsp/html_full.jsp Tue Dec 26 17:36:47 2017 +0100 @@ -0,0 +1,85 @@ +<%-- +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. +--%> +<%@page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" session="true" %> +<%@page import="de.uapcore.lightpit.Constants" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + +<%-- Define an alias for the main menu --%> +<c:set scope="page" var="mainMenu" value="${requestScope[Constants.REQ_ATTR_MENU]}"/> + +<%-- Define an alias for the fragment name --%> +<c:set scope="page" var="fragment" value="${requestScope[Constants.REQ_ATTR_FRAGMENT]}"/> + +<%-- Define an alias for the additional stylesheet --%> +<c:set scope="page" var="extraCss" value="${requestScope[Constants.REQ_ATTR_STYLESHEET]}"/> + +<%-- Define an alias for the module info --%> +<c:set scope="page" var="moduleInfo" value="${requestScope[Constants.REQ_ATTR_MODULE_INFO]}"/> + +<!DOCTYPE html> +<html> + <head> + <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/"> + <title>LightPIT - + <fmt:bundle basename="${moduleInfo.bundleBaseName}"> + <fmt:message key="${moduleInfo.titleKey}" /> + </fmt:bundle> + </title> + <meta charset="UTF-8"> + <link rel="stylesheet" href="lightpit.css" type="text/css"> + <c:if test="${not empty extraCss}"> + <link rel="stylesheet" href="${extraCss}" type="text/css"> + </c:if> + </head> + <body> + <div id="mainMenu"> + <c:forEach var="menu" items="${mainMenu}"> + <div class="menuEntry" + <c:if test="${requestScope[Constants.REQ_ATTR_MODULE_CLASSNAME] eq menu.moduleClassName}"> + data-active + </c:if> + > + <a href="${menu.pathName}"> + <fmt:bundle basename="${menu.resourceKey.bundle}"> + <fmt:message key="${menu.resourceKey.key}" /> + </fmt:bundle> + </a> + </div> + </c:forEach> + </div> + <div id="subMenu"> + + </div> + <div id="content-area"> + <c:if test="${not empty fragment}"> + <c:import url="${fragment}" /> + </c:if> + </div> + </body> +</html>
--- a/web/WEB-INF/web.xml Sat Dec 23 17:28:19 2017 +0100 +++ b/web/WEB-INF/web.xml Tue Dec 26 17:36:47 2017 +0100 @@ -5,4 +5,8 @@ 30 </session-timeout> </session-config> + <context-param> + <param-name>available-languages</param-name> + <param-value>en,de</param-value> + </context-param> </web-app>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/language.css Tue Dec 26 17:36:47 2017 +0100 @@ -0,0 +1,39 @@ +/* + * 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. + * + */ + +#lang-selector { + max-width: 30%; + display: flex; + flex-basis: content; + flex-direction: column; +} + +input { + margin: .5em; +}