Fri, 22 May 2020 16:21:31 +0200
adds breadcrumb menu
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Tue May 19 19:34:57 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Fri May 22 16:21:31 2020 +0200 @@ -230,12 +230,24 @@ * @param req the servlet request object * @param fragmentName the name of the fragment * @see Constants#DYN_FRAGMENT_PATH_PREFIX + * @see Constants#REQ_ATTR_FRAGMENT */ protected void setDynamicFragment(HttpServletRequest req, String fragmentName) { req.setAttribute(Constants.REQ_ATTR_FRAGMENT, Functions.dynFragmentPath(fragmentName)); } /** + * Sets the breadcrumbs menu. + * + * @param req the servlet request object + * @param breadcrumbs the menu entries for the breadcrumbs menu + * @see Constants#REQ_ATTR_BREADCRUMBS + */ + protected void setBreadcrumbs(HttpServletRequest req, List<MenuEntry> breadcrumbs) { + req.setAttribute(Constants.REQ_ATTR_BREADCRUMBS, breadcrumbs); + } + + /** * @param req the servlet request object * @param location the location where to redirect * @see Constants#REQ_ATTR_REDIRECT_LOCATION @@ -268,16 +280,16 @@ * The specified type must have a single-argument constructor accepting a string to perform conversion. * The constructor of the specified type may throw an exception on conversion failures. * - * @param req the servlet request object + * @param req the servlet request object * @param clazz the class object of the expected type - * @param name the name of the parameter - * @param <T> the expected type + * @param name the name of the parameter + * @param <T> the expected type * @return the parameter value or an empty optional, if no parameter with the specified name was found */ - protected<T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) { + protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) { final String paramValue = req.getParameter(name); if (paramValue == null) return Optional.empty(); - if (clazz.equals(String.class)) return Optional.of((T)paramValue); + if (clazz.equals(String.class)) return Optional.of((T) paramValue); try { final Constructor<T> ctor = clazz.getConstructor(String.class); return Optional.of(ctor.newInstance(paramValue)); @@ -290,16 +302,16 @@ /** * Tries to look up an entity with a key obtained from a request parameter. * - * @param req the servlet request object + * @param req the servlet request object * @param clazz the class representing the type of the request parameter - * @param name the name of the request parameter - * @param find the find function (typically a DAO function) - * @param <T> the type of the request parameter - * @param <R> the type of the looked up entity + * @param name the name of the request parameter + * @param find the find function (typically a DAO function) + * @param <T> the type of the request parameter + * @param <R> the type of the looked up entity * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing * @throws SQLException if the find function throws an exception */ - protected<T,R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, SQLFindFunction<? super T, ? extends R> find) throws SQLException { + protected <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, SQLFindFunction<? super T, ? extends R> find) throws SQLException { final var param = getParameter(req, clazz, name); if (param.isPresent()) { return Optional.ofNullable(find.apply(param.get())); @@ -311,7 +323,13 @@ private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu()); + final var mainMenu = new ArrayList<MenuEntry>(getModuleManager().getMainMenu()); + for (var entry : mainMenu) { + if (Functions.fullPath(req).startsWith("/" + entry.getPathName())) { + entry.setActive(true); + } + } + req.setAttribute(Constants.REQ_ATTR_MENU, mainMenu); req.getRequestDispatcher(SITE_JSP).forward(req, resp); }
--- a/src/main/java/de/uapcore/lightpit/Constants.java Tue May 19 19:34:57 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/Constants.java Fri May 22 16:21:31 2020 +0200 @@ -68,6 +68,11 @@ public static final String REQ_ATTR_MENU = fqn(AbstractLightPITServlet.class, "mainMenu"); /** + * Key for the request attribute containing the breadcrumb menu. + */ + public static final String REQ_ATTR_BREADCRUMBS = fqn(AbstractLightPITServlet.class, "breadcrumbs"); + + /** * Key for the request attribute containing the base href. */ public static final String REQ_ATTR_BASE_HREF = fqn(AbstractLightPITServlet.class, "base_href");
--- a/src/main/java/de/uapcore/lightpit/MenuEntry.java Tue May 19 19:34:57 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/MenuEntry.java Fri May 22 16:21:31 2020 +0200 @@ -45,6 +45,11 @@ private final ResourceKey resourceKey; /** + * Custom menu text. + */ + private final String text; + + /** * Path name of the module, linked by this menu entry. */ private final String pathName; @@ -54,20 +59,33 @@ */ private final int sequence; + /** + * True if this menu entry is active. + */ + private boolean active = false; + public MenuEntry(ResourceKey resourceKey, String pathName, int sequence) { + this.text = null; this.resourceKey = resourceKey; this.pathName = pathName; this.sequence = sequence; } - public MenuEntry(ResourceKey resourceKey, String pathName) { - this(resourceKey, pathName, 0); + public MenuEntry(String text, String pathName, int sequence) { + this.text = text; + this.resourceKey = null; + this.pathName = pathName; + this.sequence = sequence; } public ResourceKey getResourceKey() { return resourceKey; } + public String getText() { + return text; + } + public String getPathName() { return pathName; } @@ -76,6 +94,14 @@ return sequence; } + public boolean isActive() { + return this.active; + } + + public void setActive(boolean active) { + this.active = true; + } + @Override public boolean equals(Object o) { if (this == o) return true;
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Tue May 19 19:34:57 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Fri May 22 16:21:31 2020 +0200 @@ -40,7 +40,10 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; +import java.util.Optional; import static de.uapcore.lightpit.Functions.fqn; @@ -63,14 +66,44 @@ final var projectDao = dao.getProjectDao(); final var session = req.getSession(); final var projectSelection = getParameter(req, Integer.class, "pid"); + final Project selectedProject; if (projectSelection.isPresent()) { - final var selectedId = projectSelection.get(); - final var selectedProject = projectDao.find(selectedId); - session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, selectedProject); - return selectedProject; + selectedProject = projectDao.find(projectSelection.get()); } else { - return (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); + final var sessionProject = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); + selectedProject = sessionProject == null ? null : projectDao.find(sessionProject.getId()); } + session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, selectedProject); + return selectedProject; + } + + + /** + * Creates the breadcrumb menu. + * + * @param level the current active level + * @param selectedProject the selected project, if any, or null + * @return a dynamic breadcrumb menu trying to display as many levels as possible + */ + private List<MenuEntry> getBreadcrumbs(int level, + Project selectedProject) { + MenuEntry entry; + + final var breadcrumbs = new ArrayList<MenuEntry>(); + entry = new MenuEntry(new ResourceKey("localization.projects", "menuLabel"), + "projects/", 0); + breadcrumbs.add(entry); + if (level == 0) entry.setActive(true); + + if (selectedProject == null) + return breadcrumbs; + + entry = new MenuEntry(selectedProject.getName(), + "projects/view?pid=" + selectedProject.getId(), 1); + if (level == 1) entry.setActive(true); + + breadcrumbs.add(entry); + return breadcrumbs; } @RequestMapping(method = HttpMethod.GET) @@ -81,20 +114,33 @@ setDynamicFragment(req, "projects"); setStylesheet(req, "projects"); - if (getSelectedProject(req, dao) == null) { - projectList.stream().findFirst().ifPresent(proj -> req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, proj)); - } + final var selectedProject = getSelectedProject(req, dao); + setBreadcrumbs(req, getBreadcrumbs(0, selectedProject)); return ResponseType.HTML; } + private void configureEditForm(HttpServletRequest req, DataAccessObjects dao, Optional<Project> project) throws SQLException { + if (project.isPresent()) { + req.setAttribute("project", project.get()); + setBreadcrumbs(req, getBreadcrumbs(1, project.get())); + } else { + req.setAttribute("project", new Project(-1)); + setBreadcrumbs(req, getBreadcrumbs(0, null)); + } + + req.setAttribute("users", dao.getUserDao().list()); + setDynamicFragment(req, "project-form"); + } + @RequestMapping(requestPath = "edit", method = HttpMethod.GET) public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException { - req.setAttribute("project", findByParameter(req, Integer.class, "id", - dao.getProjectDao()::find).orElse(new Project(-1))); - req.setAttribute("users", dao.getUserDao().list()); - setDynamicFragment(req, "project-form"); + Optional<Project> project = findByParameter(req, Integer.class, "id", dao.getProjectDao()::find); + configureEditForm(req, dao, project); + if (project.isPresent()) { + req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, project.get()); + } return ResponseType.HTML; } @@ -102,7 +148,7 @@ @RequestMapping(requestPath = "commit", method = HttpMethod.POST) public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) throws SQLException { - Project project = new Project(-1); + Project project = null; try { project = new Project(getParameter(req, Integer.class, "id").orElseThrow()); project.setName(getParameter(req, String.class, "name").orElseThrow()); @@ -119,11 +165,9 @@ LOG.debug("Successfully updated project {}", project.getName()); } catch (NoSuchElementException | NumberFormatException | SQLException ex) { // TODO: set request attribute with error text - req.setAttribute("project", project); - req.setAttribute("users", dao.getUserDao().list()); - setDynamicFragment(req, "project-form"); LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex); + configureEditForm(req, dao, Optional.ofNullable(project)); } return ResponseType.HTML; @@ -140,11 +184,21 @@ req.setAttribute("versions", dao.getVersionDao().list(selectedProject)); req.setAttribute("issues", dao.getIssueDao().list(selectedProject)); + // TODO: add more levels depending on last visited location + setBreadcrumbs(req, getBreadcrumbs(1, selectedProject)); + setDynamicFragment(req, "project-details"); return ResponseType.HTML; } + private void configureEditVersionForm(HttpServletRequest req, Optional<Version> version, Project selectedProject) { + req.setAttribute("version", version.orElse(new Version(-1, selectedProject))); + req.setAttribute("versionStatusEnum", VersionStatus.values()); + + setDynamicFragment(req, "version-form"); + } + @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET) public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { final var selectedProject = getSelectedProject(req, dao); @@ -153,11 +207,9 @@ return ResponseType.NONE; } - req.setAttribute("version", findByParameter(req, Integer.class, "id", - dao.getVersionDao()::find).orElse(new Version(-1, selectedProject))); - req.setAttribute("versionStatusEnum", VersionStatus.values()); - - setDynamicFragment(req, "version-form"); + configureEditVersionForm(req, + findByParameter(req, Integer.class, "id", dao.getVersionDao()::find), + selectedProject); return ResponseType.HTML; } @@ -170,7 +222,7 @@ return ResponseType.NONE; } - Version version = new Version(-1, selectedProject); + Version version = null; try { version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), selectedProject); version.setName(getParameter(req, String.class, "name").orElseThrow()); @@ -183,16 +235,24 @@ LOG.debug("Successfully updated version {} for project {}", version.getName(), selectedProject.getName()); } catch (NoSuchElementException | NumberFormatException | SQLException ex) { // TODO: set request attribute with error text - req.setAttribute("version", version); - req.setAttribute("versionStatusEnum", VersionStatus.values()); - setDynamicFragment(req, "version-form"); LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex); + configureEditVersionForm(req, Optional.ofNullable(version), selectedProject); } return ResponseType.HTML; } + private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, Optional<Issue> issue, Project selectedProject) throws SQLException { + + req.setAttribute("issue", issue.orElse(new Issue(-1, selectedProject))); + req.setAttribute("issueStatusEnum", IssueStatus.values()); + req.setAttribute("issueCategoryEnum", IssueCategory.values()); + req.setAttribute("versions", dao.getVersionDao().list(selectedProject)); + + setDynamicFragment(req, "issue-form"); + } + @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET) public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { final var selectedProject = getSelectedProject(req, dao); @@ -201,12 +261,9 @@ return ResponseType.NONE; } - req.setAttribute("issue", findByParameter(req, Integer.class, "id", - dao.getIssueDao()::find).orElse(new Issue(-1, selectedProject))); - req.setAttribute("issueStatusEnum", IssueStatus.values()); - req.setAttribute("issueCategoryEnum", IssueCategory.values()); - - setDynamicFragment(req, "issue-form"); + configureEditIssueForm(req, dao, + findByParameter(req, Integer.class, "id", dao.getIssueDao()::find), + selectedProject); return ResponseType.HTML; } @@ -219,7 +276,7 @@ return ResponseType.NONE; } - Issue issue = new Issue(-1, selectedProject); + Issue issue = null; try { issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), selectedProject); @@ -232,12 +289,9 @@ LOG.debug("Successfully updated issue {} for project {}", issue.getId(), selectedProject.getName()); } catch (NoSuchElementException | NumberFormatException | SQLException ex) { // TODO: set request attribute with error text - req.setAttribute("issue", issue); - req.setAttribute("issueStatusEnum", IssueStatus.values()); - req.setAttribute("issueCategoryEnum", IssueCategory.values()); - setDynamicFragment(req, "issue-form"); LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex); + configureEditIssueForm(req, dao, Optional.ofNullable(issue), selectedProject); } return ResponseType.HTML;
--- a/src/main/webapp/WEB-INF/jsp/site.jsp Tue May 19 19:34:57 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp Fri May 22 16:21:31 2020 +0200 @@ -32,7 +32,7 @@ <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%-- Make the base href easily available at request scope --%> -<c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}" /> +<c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/> <%-- Define an alias for the request path --%> <c:set scope="page" var="requestPath" value="${requestScope[Constants.REQ_ATTR_PATH]}"/> @@ -40,6 +40,9 @@ <%-- Define an alias for the main menu --%> <c:set scope="page" var="mainMenu" value="${requestScope[Constants.REQ_ATTR_MENU]}"/> +<%-- Define an alias for the main menu --%> +<c:set scope="page" var="breadcrumbs" value="${requestScope[Constants.REQ_ATTR_BREADCRUMBS]}"/> + <%-- Define an alias for the fragment name --%> <c:set scope="page" var="fragment" value="${requestScope[Constants.REQ_ATTR_FRAGMENT]}"/> @@ -54,7 +57,7 @@ <%-- Apply the session locale (should always be present, but check nevertheless) --%> <c:if test="${not empty sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"> -<fmt:setLocale scope="request" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"/> + <fmt:setLocale scope="request" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"/> </c:if> <%-- Selected project, if any --%> @@ -62,48 +65,41 @@ <!DOCTYPE html> <html> - <head> - <base href="${baseHref}"> - <title>LightPIT - - <fmt:bundle basename="${moduleInfo.bundleBaseName}"> - <fmt:message key="${moduleInfo.titleKey}" /> - </fmt:bundle> - </title> - <meta charset="UTF-8"> - <c:if test="${not empty redirectLocation}"> +<head> + <base href="${baseHref}"> + <title>LightPIT - + <fmt:bundle basename="${moduleInfo.bundleBaseName}"> + <fmt:message key="${moduleInfo.titleKey}"/> + </fmt:bundle> + </title> + <meta charset="UTF-8"> + <c:if test="${not empty redirectLocation}"> <meta http-equiv="refresh" content="0; URL=${redirectLocation}"> - </c:if> - <link rel="stylesheet" href="lightpit.css" type="text/css"> - <c:if test="${not empty extraCss}"> + </c:if> + <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:set var="menuPath" value="/${menu.pathName}"/> - <c:if test="${fn:startsWith(requestPath, menuPath)}"> - 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="breadcrumbs"> - <%-- TODO: find a strategy to define the breadcrumbs --%> - </div> - <div id="content-area"> - <c:if test="${not empty fragment}"> - <fmt:setBundle scope="request" basename="${moduleInfo.bundleBaseName}"/> - <fmt:setBundle scope="request" var="lightpit_bundle" basename="localization.lightpit"/> - <c:import url="${fragment}" /> - </c:if> - </div> - </body> + </c:if> +</head> +<body> +<div id="mainMenu"> + <c:forEach var="menu" items="${mainMenu}"> + <%@include file="../jspf/menu-entry.jsp" %> + </c:forEach> +</div> +<c:if test="${not empty breadcrumbs}"> + <div id="breadcrumbs"> + <c:forEach var="menu" items="${breadcrumbs}"> + <%@include file="../jspf/menu-entry.jsp" %> + </c:forEach> + </div> +</c:if> +<div id="content-area"> + <c:if test="${not empty fragment}"> + <fmt:setBundle scope="request" basename="${moduleInfo.bundleBaseName}"/> + <fmt:setBundle scope="request" var="lightpit_bundle" basename="localization.lightpit"/> + <c:import url="${fragment}"/> + </c:if> +</div> +</body> </html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jspf/menu-entry.jsp Fri May 22 16:21:31 2020 +0200 @@ -0,0 +1,13 @@ +<div class="menuEntry" + <c:if test="${menu.active}">data-active</c:if> > + <a href="${menu.pathName}"> + <c:if test="${empty menu.resourceKey}"> + <c:out value="${menu.text}"/> + </c:if> + <c:if test="${not empty menu.resourceKey}"> + <fmt:bundle basename="${menu.resourceKey.bundle}"> + <fmt:message key="${menu.resourceKey.key}"/> + </fmt:bundle> + </c:if> + </a> +</div> \ No newline at end of file
--- a/src/main/webapp/error.css Tue May 19 19:34:57 2020 +0200 +++ b/src/main/webapp/error.css Fri May 22 16:21:31 2020 +0200 @@ -33,15 +33,15 @@ #error-page table { width: 100%; - + border-top-style: solid; border-top-width: 1pt; border-top-color: #606060; - + border-bottom-style: solid; border-bottom-width: 1pt; border-bottom-color: #505050; - + border-collapse: separate; border-spacing: .5em; }