# HG changeset patch # User Mike Becker # Date 1590157291 -7200 # Node ID dca186d3911fd0b04fe132221f0f063d68050f82 # Parent 821c4950b619b1ffe426032fa032c68f37dc9ced adds breadcrumb menu diff -r 821c4950b619 -r dca186d3911f src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java --- 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 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 the expected type + * @param name the name of the parameter + * @param the expected type * @return the parameter value or an empty optional, if no parameter with the specified name was found */ - protected Optional getParameter(HttpServletRequest req, Class clazz, String name) { + protected Optional getParameter(HttpServletRequest req, Class 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 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 the type of the request parameter - * @param 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 the type of the request parameter + * @param 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 Optional findByParameter(HttpServletRequest req, Class clazz, String name, SQLFindFunction find) throws SQLException { + protected Optional findByParameter(HttpServletRequest req, Class clazz, String name, SQLFindFunction 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(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); } diff -r 821c4950b619 -r dca186d3911f src/main/java/de/uapcore/lightpit/Constants.java --- 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"); diff -r 821c4950b619 -r dca186d3911f src/main/java/de/uapcore/lightpit/MenuEntry.java --- 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; diff -r 821c4950b619 -r dca186d3911f src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java --- 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 getBreadcrumbs(int level, + Project selectedProject) { + MenuEntry entry; + + final var breadcrumbs = new ArrayList(); + 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) 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 = 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, 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, 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; diff -r 821c4950b619 -r dca186d3911f src/main/webapp/WEB-INF/jsp/site.jsp --- 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 --%> - + <%-- Define an alias for the request path --%> @@ -40,6 +40,9 @@ <%-- Define an alias for the main menu --%> +<%-- Define an alias for the main menu --%> + + <%-- Define an alias for the fragment name --%> @@ -54,7 +57,7 @@ <%-- Apply the session locale (should always be present, but check nevertheless) --%> - + <%-- Selected project, if any --%> @@ -62,48 +65,41 @@ - - - LightPIT - - <fmt:bundle basename="${moduleInfo.bundleBaseName}"> - <fmt:message key="${moduleInfo.titleKey}" /> - </fmt:bundle> - - - + + + LightPIT - + <fmt:bundle basename="${moduleInfo.bundleBaseName}"> + <fmt:message key="${moduleInfo.titleKey}"/> + </fmt:bundle> + + + - - - + + + - - - - - -
- - - - - -
- +
+ + + + + + +
+ + + + + +
+ diff -r 821c4950b619 -r dca186d3911f src/main/webapp/WEB-INF/jspf/menu-entry.jsp --- /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 @@ + \ No newline at end of file diff -r 821c4950b619 -r dca186d3911f src/main/webapp/error.css --- 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; }