implements simple request mapper

Sun, 17 Dec 2017 01:45:28 +0100

author
Mike Becker <universe@uap-core.de>
date
Sun, 17 Dec 2017 01:45:28 +0100
changeset 11
737ab27e37b3
parent 10
89e3e6e28b69
child 12
005d27918b57

implements simple request mapper

src/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/Constants.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/Functions.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/HttpMethod.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/LightPITModule.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/Menu.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/MenuEntry.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/ModuleManager.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/RequestMapping.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/modules/LanguageModule.java file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/resources/localization/language.properties file | annotate | diff | comparison | revisions
src/java/de/uapcore/lightpit/resources/localization/language_de.properties file | annotate | diff | comparison | revisions
web/WEB-INF/jsp/full.jsp file | annotate | diff | comparison | revisions
web/WEB-INF/view/full.jsp file | annotate | diff | comparison | revisions
web/lightpit.css file | annotate | diff | comparison | revisions
     1.1 --- a/src/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sat Dec 16 20:19:28 2017 +0100
     1.2 +++ b/src/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sun Dec 17 01:45:28 2017 +0100
     1.3 @@ -29,6 +29,12 @@
     1.4  package de.uapcore.lightpit;
     1.5  
     1.6  import java.io.IOException;
     1.7 +import java.lang.reflect.Method;
     1.8 +import java.lang.reflect.Modifier;
     1.9 +import java.util.HashMap;
    1.10 +import java.util.Map;
    1.11 +import java.util.Optional;
    1.12 +import java.util.function.BiConsumer;
    1.13  import javax.servlet.ServletException;
    1.14  import javax.servlet.http.HttpServlet;
    1.15  import javax.servlet.http.HttpServletRequest;
    1.16 @@ -43,9 +49,23 @@
    1.17  public abstract class AbstractLightPITServlet extends HttpServlet {
    1.18      
    1.19      private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class);
    1.20 +    
    1.21 +    /**
    1.22 +     * Store a reference to the annotation for quicker access.
    1.23 +     */
    1.24 +    private Optional<LightPITModule> moduleInfo = Optional.empty();
    1.25  
    1.26 +    /**
    1.27 +     * The EL proxy is necessary, because the EL resolver cannot handle annotation properties.
    1.28 +     */
    1.29 +    private Optional<LightPITModule.ELProxy> moduleInfoELProxy = Optional.empty();
    1.30      
    1.31      /**
    1.32 +     * Invocation mapping gathered from the {@link RequestMapping} annotations.
    1.33 +     */
    1.34 +    private final Map<HttpMethod, Map<String, BiConsumer<HttpServletRequest, HttpServletResponse>>> mappings = new HashMap<>();
    1.35 +
    1.36 +    /**
    1.37       * Gives implementing modules access to the {@link ModuleManager}.
    1.38       * @return the module manager
    1.39       */
    1.40 @@ -53,25 +73,114 @@
    1.41          return (ModuleManager) getServletContext().getAttribute(ModuleManager.SC_ATTR_NAME);
    1.42      }
    1.43      
    1.44 -    private void addPathInformation(HttpServletRequest req) {
    1.45 -        final String path = req.getServletPath()+"/"+req.getPathInfo();
    1.46 -        req.setAttribute(Constants.REQ_ATTR_PATH, path);
    1.47 +    private void invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp) {
    1.48 +        try {
    1.49 +            LOG.debug("invoke {}", method.getName());
    1.50 +            method.invoke(this, req, resp);
    1.51 +        } catch (ReflectiveOperationException ex) {
    1.52 +            LOG.error(String.format("invocation of method %s failed", method.getName()), ex);
    1.53 +        }
    1.54 +    }
    1.55 +
    1.56 +    @Override
    1.57 +    public void init() throws ServletException {
    1.58 +        moduleInfo = Optional.ofNullable(this.getClass().getAnnotation(LightPITModule.class));
    1.59 +        moduleInfoELProxy = moduleInfo.map(LightPITModule.ELProxy::convert);
    1.60 +        
    1.61 +        if (moduleInfo.isPresent()) {
    1.62 +            Method[] methods = getClass().getDeclaredMethods();
    1.63 +            for (Method method : methods) {
    1.64 +                Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
    1.65 +                if (mapping.isPresent()) {
    1.66 +                    if (!Modifier.isPublic(method.getModifiers())) {
    1.67 +                        LOG.warn("{} is annotated with {} but is not public",
    1.68 +                                method.getName(), RequestMapping.class.getSimpleName()
    1.69 +                        );
    1.70 +                        continue;
    1.71 +                    }
    1.72 +                    if (Modifier.isAbstract(method.getModifiers())) {
    1.73 +                        LOG.warn("{} is annotated with {} but is abstract",
    1.74 +                                method.getName(), RequestMapping.class.getSimpleName()
    1.75 +                        );
    1.76 +                        continue;
    1.77 +                    }
    1.78 +                    
    1.79 +                    Class<?>[] params = method.getParameterTypes();
    1.80 +                    if (params.length == 2
    1.81 +                            && HttpServletRequest.class.isAssignableFrom(params[0])
    1.82 +                            && HttpServletResponse.class.isAssignableFrom(params[1])) {
    1.83 +                        
    1.84 +                        if (mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>()).
    1.85 +                                putIfAbsent(mapping.get().requestPath(),
    1.86 +                                        (req, resp) -> invokeMapping(method, req, resp)) != null) {
    1.87 +                            LOG.warn("{} {} has multiple mappings",
    1.88 +                                    mapping.get().method(),
    1.89 +                                    mapping.get().requestPath()
    1.90 +                            );
    1.91 +                        }
    1.92 +                        
    1.93 +                        LOG.info("{} {} maps to {}",
    1.94 +                                mapping.get().method(),
    1.95 +                                mapping.get().requestPath(),
    1.96 +                                method.getName()
    1.97 +                        );
    1.98 +                    } else {
    1.99 +                        LOG.warn("{} is annotated with {} but has the wrong signature - (HttpServletRequest,HttpServletResponse) required",
   1.100 +                                method.getName(), RequestMapping.class.getSimpleName()
   1.101 +                        );
   1.102 +                    }
   1.103 +                }
   1.104 +            }
   1.105 +        }
   1.106 +        
   1.107 +        LOG.trace("{} initialized", getServletName());
   1.108 +    }
   1.109 +
   1.110 +    @Override
   1.111 +    public void destroy() {
   1.112 +        mappings.clear();
   1.113 +        LOG.trace("{} destroyed", getServletName());
   1.114 +    }
   1.115 +    
   1.116 +    
   1.117 +    /**
   1.118 +     * Sets several requests attributes, that can be used by the JSP.
   1.119 +     * 
   1.120 +     * @param req the servlet request object
   1.121 +     * @see Constants#REQ_ATTR_PATH
   1.122 +     * @see Constants#REQ_ATTR_MODULE_CLASSNAME
   1.123 +     * @see Constants#REQ_ATTR_MODULE_INFO
   1.124 +     */
   1.125 +    private void setGenericRequestAttributes(HttpServletRequest req) {
   1.126 +        req.setAttribute(Constants.REQ_ATTR_PATH, Functions.fullPath(req));
   1.127 +
   1.128 +        req.setAttribute(Constants.REQ_ATTR_MODULE_CLASSNAME, this.getClass().getName());
   1.129 +
   1.130 +        moduleInfoELProxy.ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
   1.131      }
   1.132      
   1.133      private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp)
   1.134              throws IOException, ServletException {
   1.135          
   1.136 -        addPathInformation(req);
   1.137 -        
   1.138 -        final ModuleManager mm = getModuleManager();
   1.139 -        req.setAttribute(Constants.REQ_ATTR_MENU, mm.getMainMenu());
   1.140 -        
   1.141 +        setGenericRequestAttributes(req);
   1.142 +        req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu());
   1.143          req.getRequestDispatcher(Functions.jspPath("full.jsp")).forward(req, resp);
   1.144      }
   1.145      
   1.146 +    private Optional<BiConsumer<HttpServletRequest, HttpServletResponse>> findMapping(HttpMethod method, HttpServletRequest req) {
   1.147 +        return Optional.ofNullable(mappings.get(method)).map(
   1.148 +                (rm) -> rm.get(Optional.ofNullable(req.getPathInfo()).orElse(""))
   1.149 +        );
   1.150 +    }
   1.151 +    
   1.152      @Override
   1.153      protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
   1.154              throws ServletException, IOException {
   1.155 +        
   1.156 +        findMapping(HttpMethod.GET, req).ifPresent((consumer) -> consumer.accept(req, resp));
   1.157 +        
   1.158 +        // TODO: let the invoked handler decide (signature must be changed from a BiConsumer to a BiFunction)
   1.159 +        // TODO: we should call a default handler, if no specific mapping could be found
   1.160          forwardToFullView(req, resp);
   1.161      }
   1.162  
   1.163 @@ -79,6 +188,8 @@
   1.164      protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
   1.165              throws ServletException, IOException {
   1.166          
   1.167 +        findMapping(HttpMethod.POST, req).ifPresent((consumer) -> consumer.accept(req, resp));
   1.168 +        
   1.169          forwardToFullView(req, resp);
   1.170      }
   1.171  }
     2.1 --- a/src/java/de/uapcore/lightpit/Constants.java	Sat Dec 16 20:19:28 2017 +0100
     2.2 +++ b/src/java/de/uapcore/lightpit/Constants.java	Sun Dec 17 01:45:28 2017 +0100
     2.3 @@ -28,16 +28,33 @@
     2.4   */
     2.5  package de.uapcore.lightpit;
     2.6  
     2.7 -import static de.uapcore.lightpit.Functions.fullyQualifiedName;
     2.8 +import static de.uapcore.lightpit.Functions.fqn;
     2.9  
    2.10  /**
    2.11   * Contains all constants used by the this application.
    2.12   */
    2.13  public final class Constants {
    2.14 -    public static final String JSP_PATH_PREFIX = "/WEB-INF/view/";
    2.15 +    public static final String JSP_PATH_PREFIX = "/WEB-INF/jsp/";
    2.16      
    2.17 -    public static final String REQ_ATTR_MENU = fullyQualifiedName(AbstractLightPITServlet.class, "mainMenu");
    2.18 -    public static final String REQ_ATTR_PATH = fullyQualifiedName(AbstractLightPITServlet.class, "path");
    2.19 +    /**
    2.20 +     * Key for the request attribute containing the class name of the currently dispatching module.
    2.21 +     */
    2.22 +    public static final String REQ_ATTR_MODULE_CLASSNAME = fqn(AbstractLightPITServlet.class, "moduleClassname");
    2.23 +    
    2.24 +    /**
    2.25 +     * Key for the request attribute containing the {@link LightPITModule} information of the currently dispatching module.
    2.26 +     */
    2.27 +    public static final String REQ_ATTR_MODULE_INFO = fqn(AbstractLightPITServlet.class, "moduleInfo");
    2.28 +    
    2.29 +    /**
    2.30 +     * Key for the request attribute containing the menu list.
    2.31 +     */
    2.32 +    public static final String REQ_ATTR_MENU = fqn(AbstractLightPITServlet.class, "mainMenu");
    2.33 +    
    2.34 +    /**
    2.35 +     * Key for the request attribute containing the full path information (servlet path + path info).
    2.36 +     */
    2.37 +    public static final String REQ_ATTR_PATH = fqn(AbstractLightPITServlet.class, "path");
    2.38      
    2.39      /**
    2.40       * This class is not instantiatable.
     3.1 --- a/src/java/de/uapcore/lightpit/Functions.java	Sat Dec 16 20:19:28 2017 +0100
     3.2 +++ b/src/java/de/uapcore/lightpit/Functions.java	Sun Dec 17 01:45:28 2017 +0100
     3.3 @@ -28,6 +28,8 @@
     3.4   */
     3.5  package de.uapcore.lightpit;
     3.6  
     3.7 +import java.util.Optional;
     3.8 +import javax.servlet.http.HttpServletRequest;
     3.9  import org.slf4j.Logger;
    3.10  import org.slf4j.LoggerFactory;
    3.11  
    3.12 @@ -42,12 +44,27 @@
    3.13          return Constants.JSP_PATH_PREFIX + filename;
    3.14      }
    3.15      
    3.16 -    public static String fullyQualifiedName(String base, String name) {
    3.17 +    public static String fqn(String base, String name) {
    3.18          return base+"."+name;
    3.19      }
    3.20      
    3.21 -    public static String fullyQualifiedName(Class clazz, String name) {
    3.22 -        return fullyQualifiedName(clazz.getName(), name);
    3.23 +    public static String fqn(Class clazz, String name) {
    3.24 +        return fqn(clazz.getName(), name);
    3.25 +    }
    3.26 +    
    3.27 +    public static String fullPath(LightPITModule module, RequestMapping mapping) {
    3.28 +        StringBuilder sb = new StringBuilder();
    3.29 +        sb.append(module.modulePath());
    3.30 +        sb.append('/');
    3.31 +        if (!mapping.requestPath().isEmpty()) {
    3.32 +            sb.append(mapping.requestPath().isEmpty());
    3.33 +            sb.append('/');
    3.34 +        }
    3.35 +        return sb.toString();
    3.36 +    }
    3.37 +    
    3.38 +    public static String fullPath(HttpServletRequest req) {
    3.39 +        return req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse("");
    3.40      }
    3.41      
    3.42      /**
    3.43 @@ -60,15 +77,15 @@
    3.44       * @return the module path
    3.45       */
    3.46      public static String modulePathOf(Class<? extends AbstractLightPITServlet> clazz) {
    3.47 -        LightPITModule moduleInfo = clazz.getAnnotation(LightPITModule.class);
    3.48 -        if (moduleInfo == null) {
    3.49 +        Optional<LightPITModule> moduleInfo = Optional.ofNullable(clazz.getAnnotation(LightPITModule.class));
    3.50 +        if (moduleInfo.isPresent()) {
    3.51 +            return moduleInfo.get().modulePath();
    3.52 +        } else {
    3.53              LOG.warn(
    3.54                      "{} is a LightPIT Servlet but is missing the module annotation.",
    3.55                      clazz.getName()
    3.56              );
    3.57              return "/error/404.html";
    3.58 -        } else {
    3.59 -            return moduleInfo.modulePath();
    3.60          }
    3.61      }
    3.62      
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/src/java/de/uapcore/lightpit/HttpMethod.java	Sun Dec 17 01:45:28 2017 +0100
     4.3 @@ -0,0 +1,34 @@
     4.4 +/*
     4.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     4.6 + * 
     4.7 + * Copyright 2017 Mike Becker. All rights reserved.
     4.8 + * 
     4.9 + * Redistribution and use in source and binary forms, with or without
    4.10 + * modification, are permitted provided that the following conditions are met:
    4.11 + *
    4.12 + *   1. Redistributions of source code must retain the above copyright
    4.13 + *      notice, this list of conditions and the following disclaimer.
    4.14 + *
    4.15 + *   2. Redistributions in binary form must reproduce the above copyright
    4.16 + *      notice, this list of conditions and the following disclaimer in the
    4.17 + *      documentation and/or other materials provided with the distribution.
    4.18 + *
    4.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    4.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    4.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    4.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    4.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    4.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    4.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    4.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    4.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    4.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    4.29 + * POSSIBILITY OF SUCH DAMAGE.
    4.30 + * 
    4.31 + */
    4.32 +package de.uapcore.lightpit;
    4.33 +
    4.34 +
    4.35 +public enum HttpMethod {
    4.36 +    GET, POST, PUT, DELETE, TRACE, HEAD, OPTIONS
    4.37 +}
     5.1 --- a/src/java/de/uapcore/lightpit/LightPITModule.java	Sat Dec 16 20:19:28 2017 +0100
     5.2 +++ b/src/java/de/uapcore/lightpit/LightPITModule.java	Sun Dec 17 01:45:28 2017 +0100
     5.3 @@ -71,7 +71,60 @@
     5.4      
     5.5      /**
     5.6       * Returns the properties key for the menu label.
     5.7 -     * @return the properties key relative to the base name
     5.8 +     * @return the properties key
     5.9       */
    5.10      String menuKey() default "menuLabel";
    5.11 +    
    5.12 +    /**
    5.13 +     * Returns the properties key for the page title.
    5.14 +     * 
    5.15 +     * By default this is the same as the menu label.
    5.16 +     * 
    5.17 +     * @return the properties key
    5.18 +     */
    5.19 +    String titleKey() default "menuLabel";
    5.20 +    
    5.21 +    /**
    5.22 +     * Class representing the annotation.
    5.23 +     * This is necessary, because the EL resolver cannot deal with
    5.24 +     * annotation objects.
    5.25 +     * 
    5.26 +     * Note, that only the properties which are interesting for the JSP pages
    5.27 +     * are proxied by this object.
    5.28 +     */
    5.29 +    public static class ELProxy {
    5.30 +        private final String bundleBaseName, modulePath, menuKey, titleKey;
    5.31 +        
    5.32 +        public static ELProxy convert(LightPITModule annotation) {
    5.33 +            return new ELProxy(
    5.34 +                    annotation.bundleBaseName(),
    5.35 +                    annotation.modulePath(),
    5.36 +                    annotation.menuKey(),
    5.37 +                    annotation.titleKey()
    5.38 +            );
    5.39 +        }
    5.40 +
    5.41 +        private ELProxy(String bundleBaseName, String modulePath, String menuKey, String titleKey) {
    5.42 +            this.bundleBaseName = bundleBaseName;
    5.43 +            this.modulePath = modulePath;
    5.44 +            this.menuKey = menuKey;
    5.45 +            this.titleKey = titleKey;
    5.46 +        }
    5.47 +
    5.48 +        public String getBundleBaseName() {
    5.49 +            return bundleBaseName;
    5.50 +        }
    5.51 +
    5.52 +        public String getMenuKey() {
    5.53 +            return menuKey;
    5.54 +        }
    5.55 +
    5.56 +        public String getModulePath() {
    5.57 +            return modulePath;
    5.58 +        }
    5.59 +
    5.60 +        public String getTitleKey() {
    5.61 +            return titleKey;
    5.62 +        }
    5.63 +    }
    5.64  }
     6.1 --- a/src/java/de/uapcore/lightpit/Menu.java	Sat Dec 16 20:19:28 2017 +0100
     6.2 +++ b/src/java/de/uapcore/lightpit/Menu.java	Sun Dec 17 01:45:28 2017 +0100
     6.3 @@ -44,12 +44,27 @@
     6.4      private final List<MenuEntry> entries = new ArrayList<>();
     6.5      private final List<MenuEntry> immutableEntries = Collections.unmodifiableList(entries);
     6.6      
     6.7 +    /**
     6.8 +     * Class name of the module for which this menu is built.
     6.9 +     */
    6.10 +    private String moduleClassName;
    6.11 +    
    6.12 +    
    6.13      public Menu() {
    6.14          super();
    6.15      }
    6.16      
    6.17 -    public Menu(ResourceKey resourceKey, String pathName) {
    6.18 +    public Menu(String moduleClassName, ResourceKey resourceKey, String pathName) {
    6.19          super(resourceKey, pathName);
    6.20 +        this.moduleClassName = moduleClassName;
    6.21 +    }
    6.22 +    
    6.23 +    public void setModuleClassName(String moduleClassName) {
    6.24 +        this.moduleClassName = moduleClassName;
    6.25 +    }
    6.26 +
    6.27 +    public String getModuleClassName() {
    6.28 +        return moduleClassName;
    6.29      }
    6.30  
    6.31      /**
     7.1 --- a/src/java/de/uapcore/lightpit/MenuEntry.java	Sat Dec 16 20:19:28 2017 +0100
     7.2 +++ b/src/java/de/uapcore/lightpit/MenuEntry.java	Sun Dec 17 01:45:28 2017 +0100
     7.3 @@ -37,7 +37,14 @@
     7.4   */
     7.5  public class MenuEntry {
     7.6      
     7.7 +    /**
     7.8 +     * Resource key for the menu label.
     7.9 +     */
    7.10      private ResourceKey resourceKey;
    7.11 +    
    7.12 +    /**
    7.13 +     * Path name of the module, linked by this menu entry.
    7.14 +     */
    7.15      private String pathName;
    7.16  
    7.17      public MenuEntry() {
    7.18 @@ -48,38 +55,18 @@
    7.19          this.pathName = pathName;
    7.20      }
    7.21  
    7.22 -    /**
    7.23 -     * Sets the resource key, which is used to look up the menu label.
    7.24 -     * 
    7.25 -     * @param resourceKey the key for the resource bundle
    7.26 -     */
    7.27      public void setResourceKey(ResourceKey resourceKey) {
    7.28          this.resourceKey = resourceKey;
    7.29      }
    7.30  
    7.31 -    /**
    7.32 -     * Retrieves the resource key.
    7.33 -     * 
    7.34 -     * @return the key for the resource bundle
    7.35 -     */
    7.36      public ResourceKey getResourceKey() {
    7.37          return resourceKey;
    7.38      }
    7.39  
    7.40 -    /**
    7.41 -     * Sets the path name of the web page accessed via this menu entry.
    7.42 -     * 
    7.43 -     * @param pathName path name relative to the context path
    7.44 -     */
    7.45      public void setPathName(String pathName) {
    7.46          this.pathName = pathName;
    7.47      }
    7.48  
    7.49 -    /**
    7.50 -     * Retrieves the path name of the web page accessed via this menu entry.
    7.51 -     * 
    7.52 -     * @return path name relative to the context path
    7.53 -     */
    7.54      public String getPathName() {
    7.55          return pathName;
    7.56      }
     8.1 --- a/src/java/de/uapcore/lightpit/ModuleManager.java	Sat Dec 16 20:19:28 2017 +0100
     8.2 +++ b/src/java/de/uapcore/lightpit/ModuleManager.java	Sun Dec 17 01:45:28 2017 +0100
     8.3 @@ -107,8 +107,9 @@
     8.4          }        
     8.5      }
     8.6      
     8.7 -    private void addModuleToMenu(LightPITModule moduleInfo) {
     8.8 +    private void addModuleToMenu(String moduleClassName, LightPITModule moduleInfo) {
     8.9          final Menu menu = new Menu(
    8.10 +                moduleClassName,
    8.11                  new ResourceKey(moduleInfo.bundleBaseName(), moduleInfo.menuKey()),
    8.12                  moduleInfo.modulePath()
    8.13          );
    8.14 @@ -118,7 +119,9 @@
    8.15      private void handleServletRegistration(String name, Registration reg) {
    8.16          final Optional<LightPITModule> moduleInfo = getModuleInfo(reg);
    8.17          if (moduleInfo.isPresent()) {
    8.18 -            addModuleToMenu(moduleInfo.get());
    8.19 +            
    8.20 +            // TODO: remove this call and add the module to some dependency resolver, first
    8.21 +            addModuleToMenu(reg.getClassName(), moduleInfo.get());
    8.22              
    8.23              LOG.info("Module detected: {}", name);
    8.24          } else {
     9.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     9.2 +++ b/src/java/de/uapcore/lightpit/RequestMapping.java	Sun Dec 17 01:45:28 2017 +0100
     9.3 @@ -0,0 +1,76 @@
     9.4 +/*
     9.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     9.6 + * 
     9.7 + * Copyright 2017 Mike Becker. All rights reserved.
     9.8 + * 
     9.9 + * Redistribution and use in source and binary forms, with or without
    9.10 + * modification, are permitted provided that the following conditions are met:
    9.11 + *
    9.12 + *   1. Redistributions of source code must retain the above copyright
    9.13 + *      notice, this list of conditions and the following disclaimer.
    9.14 + *
    9.15 + *   2. Redistributions in binary form must reproduce the above copyright
    9.16 + *      notice, this list of conditions and the following disclaimer in the
    9.17 + *      documentation and/or other materials provided with the distribution.
    9.18 + *
    9.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    9.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    9.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    9.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    9.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    9.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    9.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    9.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    9.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    9.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    9.29 + * POSSIBILITY OF SUCH DAMAGE.
    9.30 + * 
    9.31 + */
    9.32 +package de.uapcore.lightpit;
    9.33 +
    9.34 +import java.lang.annotation.Documented;
    9.35 +import java.lang.annotation.ElementType;
    9.36 +import java.lang.annotation.Retention;
    9.37 +import java.lang.annotation.RetentionPolicy;
    9.38 +import java.lang.annotation.Target;
    9.39 +
    9.40 +
    9.41 +/**
    9.42 + * Maps requests to methods.
    9.43 + * 
    9.44 + * This annotation is used to annotate methods within classes which
    9.45 + * override {@link AbstractLightPITServlet}.
    9.46 + */
    9.47 +@Documented
    9.48 +@Retention(RetentionPolicy.RUNTIME)
    9.49 +@Target(ElementType.METHOD)
    9.50 +public @interface RequestMapping {
    9.51 +    
    9.52 +    /**
    9.53 +     * Specifies the HTTP method.
    9.54 +     * 
    9.55 +     * @return the HTTP method handled by the annotated Java method
    9.56 +     */
    9.57 +    HttpMethod method();
    9.58 +
    9.59 +    /**
    9.60 +     * Specifies the request path relative to the module path.
    9.61 +     * 
    9.62 +     * If a menu key is specified, this is also the path, which is linked
    9.63 +     * by the menu entry.
    9.64 +     * 
    9.65 +     * The path must be specified <em>without</em> a trailing slash.
    9.66 +     * 
    9.67 +     * @return the request path the annotated method should handle
    9.68 +     */
    9.69 +    String requestPath() default "";
    9.70 +    
    9.71 +    /**
    9.72 +     * Returns the properties key for the (sub) menu label.
    9.73 +     * 
    9.74 +     * This should only be used for {@link HttpMethod#GET} requests.
    9.75 +     * 
    9.76 +     * @return the properties key
    9.77 +     */
    9.78 +    String menuKey() default "";
    9.79 +}
    10.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    10.2 +++ b/src/java/de/uapcore/lightpit/modules/LanguageModule.java	Sun Dec 17 01:45:28 2017 +0100
    10.3 @@ -0,0 +1,54 @@
    10.4 +/*
    10.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    10.6 + * 
    10.7 + * Copyright 2017 Mike Becker. All rights reserved.
    10.8 + * 
    10.9 + * Redistribution and use in source and binary forms, with or without
   10.10 + * modification, are permitted provided that the following conditions are met:
   10.11 + *
   10.12 + *   1. Redistributions of source code must retain the above copyright
   10.13 + *      notice, this list of conditions and the following disclaimer.
   10.14 + *
   10.15 + *   2. Redistributions in binary form must reproduce the above copyright
   10.16 + *      notice, this list of conditions and the following disclaimer in the
   10.17 + *      documentation and/or other materials provided with the distribution.
   10.18 + *
   10.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   10.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   10.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   10.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
   10.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   10.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   10.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   10.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   10.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   10.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   10.29 + * POSSIBILITY OF SUCH DAMAGE.
   10.30 + * 
   10.31 + */
   10.32 +package de.uapcore.lightpit.modules;
   10.33 +
   10.34 +import de.uapcore.lightpit.LightPITModule;
   10.35 +import de.uapcore.lightpit.AbstractLightPITServlet;
   10.36 +import de.uapcore.lightpit.HttpMethod;
   10.37 +import javax.servlet.annotation.WebServlet;
   10.38 +import javax.servlet.http.HttpServletRequest;
   10.39 +import javax.servlet.http.HttpServletResponse;
   10.40 +import de.uapcore.lightpit.RequestMapping;
   10.41 +
   10.42 +
   10.43 +@LightPITModule(
   10.44 +        bundleBaseName = "de.uapcore.lightpit.resources.localization.language",
   10.45 +        modulePath = "language"
   10.46 +)
   10.47 +@WebServlet(
   10.48 +        name = "LanguageModule",
   10.49 +        urlPatterns = "/language/*"
   10.50 +)
   10.51 +public final class LanguageModule extends AbstractLightPITServlet {
   10.52 +    
   10.53 +    @RequestMapping(method = HttpMethod.GET)
   10.54 +    public void handle(HttpServletRequest req, HttpServletResponse resp) {
   10.55 +        
   10.56 +    }
   10.57 +}
    11.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    11.2 +++ b/src/java/de/uapcore/lightpit/resources/localization/language.properties	Sun Dec 17 01:45:28 2017 +0100
    11.3 @@ -0,0 +1,24 @@
    11.4 +# Copyright 2017 Mike Becker. All rights reserved.
    11.5 +#
    11.6 +# Redistribution and use in source and binary forms, with or without
    11.7 +# modification, are permitted provided that the following conditions are met:
    11.8 +#
    11.9 +# 1. Redistributions of source code must retain the above copyright
   11.10 +# notice, this list of conditions and the following disclaimer.
   11.11 +#
   11.12 +# 2. Redistributions in binary form must reproduce the above copyright
   11.13 +# notice, this list of conditions and the following disclaimer in the
   11.14 +# documentation and/or other materials provided with the distribution.
   11.15 +#
   11.16 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   11.17 +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   11.18 +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   11.19 +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   11.20 +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   11.21 +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   11.22 +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   11.23 +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   11.24 +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   11.25 +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
   11.26 +
   11.27 +menuLabel = Languages
    12.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    12.2 +++ b/src/java/de/uapcore/lightpit/resources/localization/language_de.properties	Sun Dec 17 01:45:28 2017 +0100
    12.3 @@ -0,0 +1,24 @@
    12.4 +# Copyright 2017 Mike Becker. All rights reserved.
    12.5 +#
    12.6 +# Redistribution and use in source and binary forms, with or without
    12.7 +# modification, are permitted provided that the following conditions are met:
    12.8 +#
    12.9 +# 1. Redistributions of source code must retain the above copyright
   12.10 +# notice, this list of conditions and the following disclaimer.
   12.11 +#
   12.12 +# 2. Redistributions in binary form must reproduce the above copyright
   12.13 +# notice, this list of conditions and the following disclaimer in the
   12.14 +# documentation and/or other materials provided with the distribution.
   12.15 +#
   12.16 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   12.17 +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   12.18 +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   12.19 +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   12.20 +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   12.21 +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   12.22 +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   12.23 +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   12.24 +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   12.25 +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
   12.26 +
   12.27 +menuLabel = Sprache
    13.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    13.2 +++ b/web/WEB-INF/jsp/full.jsp	Sun Dec 17 01:45:28 2017 +0100
    13.3 @@ -0,0 +1,78 @@
    13.4 +<%-- 
    13.5 +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    13.6 +
    13.7 +Copyright 2017 Mike Becker. All rights reserved.
    13.8 +
    13.9 +Redistribution and use in source and binary forms, with or without
   13.10 +modification, are permitted provided that the following conditions are met:
   13.11 +
   13.12 +1. Redistributions of source code must retain the above copyright
   13.13 +notice, this list of conditions and the following disclaimer.
   13.14 +
   13.15 +2. Redistributions in binary form must reproduce the above copyright
   13.16 +notice, this list of conditions and the following disclaimer in the
   13.17 +documentation and/or other materials provided with the distribution.
   13.18 +
   13.19 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   13.20 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   13.21 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   13.22 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   13.23 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   13.24 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   13.25 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   13.26 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   13.27 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   13.28 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
   13.29 +--%>
   13.30 +<%@page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
   13.31 +<%@page import="de.uapcore.lightpit.Constants" %>
   13.32 +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   13.33 +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   13.34 +<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
   13.35 +
   13.36 +<%-- Define an alias for the main menu --%>
   13.37 +<c:set scope="page" var="mainMenu" value="${requestScope[Constants.REQ_ATTR_MENU]}"/>
   13.38 +
   13.39 +<%-- Define an alias for the module info --%>
   13.40 +<c:set scope="page" var="moduleInfo" value="${requestScope[Constants.REQ_ATTR_MODULE_INFO]}"/>
   13.41 +
   13.42 +<!DOCTYPE html>
   13.43 +<html>
   13.44 +    <head>
   13.45 +        <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">
   13.46 +        <title>LightPIT -
   13.47 +            <fmt:bundle basename="${moduleInfo.bundleBaseName}">
   13.48 +                <fmt:message key="${moduleInfo.titleKey}" />
   13.49 +            </fmt:bundle>
   13.50 +        </title>
   13.51 +        <meta charset="UTF-8">
   13.52 +        <link rel="stylesheet" href="lightpit.css" type="text/css">
   13.53 +    </head>
   13.54 +    <body>
   13.55 +        <div id="mainMenu">
   13.56 +            <c:forEach var="menu" items="${mainMenu}">
   13.57 +                <div class="menuEntry"
   13.58 +                     <c:if test="${requestScope[Constants.REQ_ATTR_MODULE_CLASSNAME] eq menu.moduleClassName}">
   13.59 +                         data-active
   13.60 +                     </c:if>
   13.61 +                >
   13.62 +                    <a href="${menu.pathName}">
   13.63 +                    <fmt:bundle basename="${menu.resourceKey.bundle}">
   13.64 +                        <fmt:message key="${menu.resourceKey.key}" />
   13.65 +                    </fmt:bundle>
   13.66 +                    </a>
   13.67 +                </div>
   13.68 +            </c:forEach>
   13.69 +            
   13.70 +        </div>
   13.71 +        <div id="subMenu">
   13.72 +            
   13.73 +        </div>
   13.74 +        <div id="content-area">
   13.75 +            <%-- Resource keys should be rooted in the specific bundle for this module. --%>
   13.76 +            <fmt:bundle basename="${moduleInfo.bundleBaseName}">
   13.77 +                TODO: load fragment
   13.78 +            </fmt:bundle>
   13.79 +        </div>
   13.80 +    </body>
   13.81 +</html>
    14.1 --- a/web/WEB-INF/view/full.jsp	Sat Dec 16 20:19:28 2017 +0100
    14.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    14.3 @@ -1,69 +0,0 @@
    14.4 -<%-- 
    14.5 -DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    14.6 -
    14.7 -Copyright 2017 Mike Becker. All rights reserved.
    14.8 -
    14.9 -Redistribution and use in source and binary forms, with or without
   14.10 -modification, are permitted provided that the following conditions are met:
   14.11 -
   14.12 -1. Redistributions of source code must retain the above copyright
   14.13 -notice, this list of conditions and the following disclaimer.
   14.14 -
   14.15 -2. Redistributions in binary form must reproduce the above copyright
   14.16 -notice, this list of conditions and the following disclaimer in the
   14.17 -documentation and/or other materials provided with the distribution.
   14.18 -
   14.19 -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   14.20 -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   14.21 -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   14.22 -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   14.23 -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   14.24 -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   14.25 -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   14.26 -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   14.27 -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   14.28 -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
   14.29 ---%>
   14.30 -<%@page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
   14.31 -<%@page import="de.uapcore.lightpit.Constants" %>
   14.32 -<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   14.33 -<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   14.34 -<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
   14.35 -
   14.36 -<%-- Define an alias for the main menu --%>
   14.37 -<c:set scope="page" var="mainMenu" value="${requestScope[Constants.REQ_ATTR_MENU]}"/>
   14.38 -
   14.39 -<!DOCTYPE html>
   14.40 -<html>
   14.41 -    <head>
   14.42 -        <base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">
   14.43 -        <title>LightPIT - TODO: current menu</title>
   14.44 -        <meta charset="UTF-8">
   14.45 -        <link rel="stylesheet" href="lightpit.css" type="text/css">
   14.46 -    </head>
   14.47 -    <body>
   14.48 -        
   14.49 -        <div id="mainMenu">
   14.50 -            <c:forEach var="menuEntry" items="${mainMenu}">
   14.51 -                <div class="menuEntry"
   14.52 -                     <c:if test="${fn:contains(requestScope[Constants.REQ_ATTR_PATH], menuEntry.pathName)}">
   14.53 -                         data-active
   14.54 -                     </c:if>
   14.55 -                >
   14.56 -                    <a href="${menuEntry.pathName}">
   14.57 -                    <fmt:bundle basename="${menuEntry.resourceKey.bundle}">
   14.58 -                        <fmt:message key="${menuEntry.resourceKey.key}" />
   14.59 -                    </fmt:bundle>
   14.60 -                    </a>
   14.61 -                </div>
   14.62 -            </c:forEach>
   14.63 -            
   14.64 -        </div>
   14.65 -        <div id="subMenu">
   14.66 -            
   14.67 -        </div>
   14.68 -        <div id="content-area">
   14.69 -        TODO: content fragment
   14.70 -        </div>
   14.71 -    </body>
   14.72 -</html>
    15.1 --- a/web/lightpit.css	Sat Dec 16 20:19:28 2017 +0100
    15.2 +++ b/web/lightpit.css	Sun Dec 17 01:45:28 2017 +0100
    15.3 @@ -43,7 +43,7 @@
    15.4  }
    15.5  
    15.6  a {
    15.7 -    color: #3030f8;
    15.8 +    color: #3060f8;
    15.9      text-decoration: none;
   15.10  }
   15.11  
   15.12 @@ -60,7 +60,7 @@
   15.13  #subMenu {
   15.14      background: #f7f7ff;
   15.15  
   15.16 -    border-image: linear-gradient(to right, #606060, rgba(60,60,60,.1));
   15.17 +    border-image: linear-gradient(to right, #606060, rgba(60,60,60,.25));
   15.18      border-image-slice: 1;
   15.19      border-top-style: solid;
   15.20      border-top-width: 1pt;

mercurial