projects can now be added and updated

Thu, 14 May 2020 22:48:01 +0200

author
Mike Becker <universe@uap-core.de>
date
Thu, 14 May 2020 22:48:01 +0200
changeset 47
57cfb94ab99f
parent 46
1574965c7dc7
child 48
63200a99ea77

projects can now be added and updated

src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/Constants.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/Functions.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/AbstractDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/Functions.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/GenericDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGDataAccessObjects.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Project.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/User.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/HomeModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/resources/localization/home.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/home_de.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/lightpit.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/lightpit_de.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/projects.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/projects_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/dynamic_fragments/commit-successful.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/dynamic_fragments/error.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/dynamic_fragments/home.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/dynamic_fragments/project-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/dynamic_fragments/projects.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/home.css file | annotate | diff | comparison | revisions
src/main/webapp/lightpit.css file | annotate | diff | comparison | revisions
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Thu May 14 22:48:01 2020 +0200
@@ -39,6 +39,7 @@
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import java.io.IOException;
+import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.sql.Connection;
@@ -225,6 +226,18 @@
     }
 
     /**
+     * @param req      the servlet request object
+     * @param location the location where to redirect
+     * @see Constants#REQ_ATTR_REDIRECT_LOCATION
+     */
+    public void setRedirectLocation(HttpServletRequest req, String location) {
+        if (location.startsWith("./")) {
+            location = location.replaceFirst("\\./", Functions.baseHref(req));
+        }
+        req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location);
+    }
+
+    /**
      * Specifies the name of an additional stylesheet used by the module.
      * <p>
      * Setting an additional stylesheet is optional, but quite common for HTML
@@ -240,6 +253,30 @@
         req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Functions.enforceExt(stylesheet, ".css"));
     }
 
+    /**
+     * Obtains a request parameter of the specified type.
+     * 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 clazz the class object of 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
+     */
+    public<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);
+        try {
+            final Constructor<T> ctor = clazz.getConstructor(String.class);
+            return Optional.of(ctor.newInstance(paramValue));
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
+
+    }
+
     private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp)
             throws IOException, ServletException {
 
@@ -287,6 +324,7 @@
         }
 
         // set some internal request attributes
+        req.setAttribute(Constants.REQ_ATTR_BASE_HREF, Functions.baseHref(req));
         req.setAttribute(Constants.REQ_ATTR_PATH, Functions.fullPath(req));
         Optional.ofNullable(moduleInfo).ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
 
--- a/src/main/java/de/uapcore/lightpit/Constants.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/Constants.java	Thu May 14 22:48:01 2020 +0200
@@ -40,6 +40,7 @@
 
     public static final String DYN_FRAGMENT_PATH_PREFIX = "/WEB-INF/dynamic_fragments/";
 
+    public static final String DYN_FRAGMENT_COMMIT_SUCCESSFUL = "commit-successful";
 
     /**
      * Name for the context parameter specifying the available languages.
@@ -77,6 +78,11 @@
     public static final String REQ_ATTR_SUB_MENU = fqn(AbstractLightPITServlet.class, "subMenu");
 
     /**
+     * Key for the request attribute containing the base href.
+     */
+    public static final String REQ_ATTR_BASE_HREF = fqn(AbstractLightPITServlet.class, "base_href");
+
+    /**
      * 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");
@@ -91,6 +97,11 @@
      */
     public static final String REQ_ATTR_STYLESHEET = fqn(AbstractLightPITServlet.class, "extraCss");
 
+    /**
+     * Key for a location the page shall redirect to.
+     * Will be used in a meta element.
+     */
+    public static final String REQ_ATTR_REDIRECT_LOCATION = fqn(AbstractLightPITServlet.class, "redirectLocation");
 
     /**
      * Key for the current language selection within the session.
--- a/src/main/java/de/uapcore/lightpit/Functions.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/Functions.java	Thu May 14 22:48:01 2020 +0200
@@ -66,15 +66,12 @@
         return fqn(clazz.getName(), name);
     }
 
-    public static String fullPath(LightPITModule module, RequestMapping mapping) {
-        StringBuilder sb = new StringBuilder();
-        sb.append(module.modulePath());
-        sb.append('/');
-        if (!mapping.requestPath().isEmpty()) {
-            sb.append(mapping.requestPath().isEmpty());
-            sb.append('/');
-        }
-        return sb.toString();
+    public static String baseHref(HttpServletRequest req) {
+        return String.format("%s://%s:%d%s/",
+                req.getScheme(),
+                req.getServerName(),
+                req.getServerPort(),
+                req.getContextPath());
     }
 
     public static String fullPath(HttpServletRequest req) {
--- a/src/main/java/de/uapcore/lightpit/dao/AbstractDao.java	Wed May 13 21:46:26 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2018 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- */
-package de.uapcore.lightpit.dao;
-
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Types;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Function;
-
-public abstract class AbstractDao<T> implements GenericDao<T> {
-
-    private final PreparedStatement listQuery;
-
-    protected AbstractDao(PreparedStatement listQuery) {
-        this.listQuery = listQuery;
-    }
-
-    public final T mapColumns(ResultSet result) throws SQLException {
-        return mapColumns(result, "");
-    }
-
-    public abstract T mapColumns(ResultSet result, String qualifier) throws SQLException;
-
-    /**
-     * Qualifies a column label if an qualifier is specified.
-     *
-     * @param qualifier an optional qualifier
-     * @param label     the column label
-     * @return the label, qualified if necessary
-     */
-    protected final String qual(String qualifier, String label) {
-        if (qualifier == null || qualifier.isBlank()) {
-            return label;
-        } else {
-            return qualifier + "." + label;
-        }
-    }
-
-    protected final void setStringOrNull(PreparedStatement stmt, int index, String str) throws SQLException {
-        if (str == null || str.isBlank()) {
-            stmt.setNull(index, Types.VARCHAR);
-        } else {
-            stmt.setString(index, str);
-        }
-    }
-
-    protected final <T> void setForeignKeyOrNull(PreparedStatement stmt, int index, T instance, Function<? super T, Integer> keyGetter) throws SQLException {
-        Integer key = Optional.ofNullable(instance).map(keyGetter).orElse(null);
-        if (key == null) {
-            stmt.setNull(index, Types.INTEGER);
-        } else {
-            stmt.setInt(index, key);
-        }
-    }
-
-    @Override
-    public List<T> list() throws SQLException {
-        List<T> list = new ArrayList<>();
-        try (ResultSet result = listQuery.executeQuery()) {
-            while (result.next()) {
-                list.add(mapColumns(result));
-            }
-        }
-        return list;
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/dao/Functions.java	Thu May 14 22:48:01 2020 +0200
@@ -0,0 +1,62 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2018 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package de.uapcore.lightpit.dao;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Some DAO utilities.
+ */
+public final class Functions {
+
+    public static void setStringOrNull(PreparedStatement stmt, int index, String str) throws SQLException {
+        if (str == null || str.isBlank()) {
+            stmt.setNull(index, Types.VARCHAR);
+        } else {
+            stmt.setString(index, str);
+        }
+    }
+
+    public static <T> void setForeignKeyOrNull(PreparedStatement stmt, int index, T instance, Function<? super T, Integer> keyGetter) throws SQLException {
+        Integer key = Optional.ofNullable(instance).map(keyGetter).orElse(null);
+        if (key == null) {
+            stmt.setNull(index, Types.INTEGER);
+        } else {
+            stmt.setInt(index, key);
+        }
+    }
+
+    private Functions() {
+
+    }
+}
--- a/src/main/java/de/uapcore/lightpit/dao/GenericDao.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/GenericDao.java	Thu May 14 22:48:01 2020 +0200
@@ -41,6 +41,15 @@
     List<T> list() throws SQLException;
 
     /**
+     * Finds an entity by its integer ID.
+     *
+     * @param id the id
+     * @return the enity or null if there is no such entity
+     * @throws SQLException on any kind of SQL errors
+     */
+    T find(int id) throws SQLException;
+
+    /**
      * Inserts an instance into database.
      *
      * @param instance the instance to insert
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGDataAccessObjects.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGDataAccessObjects.java	Thu May 14 22:48:01 2020 +0200
@@ -41,9 +41,8 @@
     private final ProjectDao projectDao;
 
     public PGDataAccessObjects(Connection connection) throws SQLException {
-        final PGUserDao pgUserDao = new PGUserDao(connection);
-        userDao = pgUserDao;
-        projectDao = new PGProjectDao(connection, pgUserDao);
+        userDao = new PGUserDao(connection);
+        projectDao = new PGProjectDao(connection);
     }
 
     @Override
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Thu May 14 22:48:01 2020 +0200
@@ -28,7 +28,7 @@
  */
 package de.uapcore.lightpit.dao.postgres;
 
-import de.uapcore.lightpit.dao.AbstractDao;
+import de.uapcore.lightpit.dao.GenericDao;
 import de.uapcore.lightpit.dao.ProjectDao;
 import de.uapcore.lightpit.entities.Project;
 import de.uapcore.lightpit.entities.User;
@@ -37,18 +37,31 @@
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
-public final class PGProjectDao extends AbstractDao<Project> implements ProjectDao {
+import static de.uapcore.lightpit.dao.Functions.setForeignKeyOrNull;
+import static de.uapcore.lightpit.dao.Functions.setStringOrNull;
 
-    private final PGUserDao userDao;
+public final class PGProjectDao implements ProjectDao, GenericDao<Project> {
+
+    private final PreparedStatement insert, update, list, find;
 
-    private final PreparedStatement insert;
-    private final PreparedStatement update;
+    public PGProjectDao(Connection connection) throws SQLException {
+        list = connection.prepareStatement(
+                "select id, name, description, repourl, " +
+                        "userid, username, lastname, givenname, mail " +
+                        "from lpit_project " +
+                        "left join lpit_user owner on lpit_project.owner = owner.userid " +
+                        "order by name");
 
-    public PGProjectDao(Connection connection, PGUserDao userDao) throws SQLException {
-        super(connection.prepareStatement(
-                "select * from lpit_project join lpit_user owner on lpit_project.owner = owner.userid"));
+        find = connection.prepareStatement(
+                "select id, name, description, repourl, " +
+                        "userid, username, lastname, givenname, mail " +
+                        "from lpit_project " +
+                        "left join lpit_user owner on lpit_project.owner = owner.userid " +
+                        "where id = ?");
 
         insert = connection.prepareStatement(
                 "insert into lpit_project (name, description, repourl, owner) values (?, ?, ?, ?)"
@@ -56,17 +69,24 @@
         update = connection.prepareStatement(
                 "update lpit_project set name = ?, description = ?, repourl = ?, owner = ? where id = ?"
         );
-
-        this.userDao = userDao;
     }
 
-    @Override
-    public Project mapColumns(ResultSet result, String q) throws SQLException {
-        final var proj = new Project(result.getInt(qual(q, "id")));
-        proj.setName(result.getString(qual(q, "name")));
-        proj.setDescription(result.getString(qual(q, "description")));
-        proj.setRepoUrl(result.getString(qual(q, "repourl")));
-        proj.setOwner(userDao.mapColumns(result, "owner"));
+    public Project mapColumns(ResultSet result) throws SQLException {
+        final var proj = new Project(result.getInt("id"));
+        proj.setName(result.getString("name"));
+        proj.setDescription(result.getString("description"));
+        proj.setRepoUrl(result.getString("repourl"));
+
+        final int id = result.getInt("userid");
+        if (id != 0) {
+            final var user = new User(id);
+            user.setUsername(result.getString("username"));
+            user.setGivenname(result.getString("givenname"));
+            user.setLastname(result.getString("lastname"));
+            user.setMail(result.getString("mail"));
+            proj.setOwner(user);
+        }
+
         return proj;
     }
 
@@ -87,6 +107,30 @@
         setStringOrNull(update, 2, instance.getDescription());
         setStringOrNull(update, 3, instance.getRepoUrl());
         setForeignKeyOrNull(update, 4, instance.getOwner(), User::getUserID);
+        update.setInt(5, instance.getId());
         return update.executeUpdate() > 0;
     }
+
+    @Override
+    public List<Project> list() throws SQLException {
+        List<Project> projects = new ArrayList<>();
+        try (var result = list.executeQuery()) {
+            while (result.next()) {
+                projects.add(mapColumns(result));
+            }
+        }
+        return projects;
+    }
+
+    @Override
+    public Project find(int id) throws SQLException {
+        find.setInt(1, id);
+        try (var result = find.executeQuery()) {
+            if (result.next()) {
+                return mapColumns(result);
+            } else {
+                return null;
+            }
+        }
+    }
 }
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java	Thu May 14 22:48:01 2020 +0200
@@ -28,7 +28,7 @@
  */
 package de.uapcore.lightpit.dao.postgres;
 
-import de.uapcore.lightpit.dao.AbstractDao;
+import de.uapcore.lightpit.dao.GenericDao;
 import de.uapcore.lightpit.dao.UserDao;
 import de.uapcore.lightpit.entities.User;
 
@@ -36,26 +36,41 @@
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
-public final class PGUserDao extends AbstractDao<User> implements UserDao {
+import static de.uapcore.lightpit.dao.Functions.setStringOrNull;
+
+public final class PGUserDao implements UserDao, GenericDao<User> {
 
-    private final PreparedStatement insert;
-    private final PreparedStatement update;
+    public static final String[] COLUMNS = {
+            "id", "username", "lastname", "givenname", "mail"
+    };
+
+    private final PreparedStatement insert, update, list, find;
 
     public PGUserDao(Connection connection) throws SQLException {
-        super(connection.prepareStatement("select * from lpit_user where userid >= 0 order by username"));
+        list = connection.prepareStatement(
+                "select userid, username, lastname, givenname, mail " +
+                        "from lpit_user where userid >= 0 " +
+                        "order by username");
+        find = connection.prepareStatement(
+                "select userid, username, lastname, givenname, mail " +
+                        "from lpit_user where userid = ? ");
 
         insert = connection.prepareStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)");
         update = connection.prepareStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?");
     }
 
-    @Override
-    public User mapColumns(ResultSet result, String q) throws SQLException {
-        final var user = new User(result.getInt(qual(q, "userid")));
-        user.setUsername(result.getString(qual(q, "username")));
-        user.setGivenname(result.getString(qual(q, "givenname")));
-        user.setLastname(result.getString(qual(q, "lastname")));
+    public User mapColumns(ResultSet result) throws SQLException {
+        final int id = result.getInt("userid");
+        if (id == 0) return null;
+        final var user = new User(id);
+        user.setUsername(result.getString("username"));
+        user.setGivenname(result.getString("givenname"));
+        user.setLastname(result.getString("lastname"));
+        user.setMail(result.getString("mail"));
         return user;
     }
 
@@ -77,4 +92,27 @@
         update.setInt(4, instance.getUserID());
         return update.executeUpdate() > 0;
     }
+
+    @Override
+    public List<User> list() throws SQLException {
+        List<User> users = new ArrayList<>();
+        try (var result = list.executeQuery()) {
+            while (result.next()) {
+                users.add(mapColumns(result));
+            }
+        }
+        return users;
+    }
+
+    @Override
+    public User find(int id) throws SQLException {
+        find.setInt(1, id);
+        try (var result = find.executeQuery()) {
+            if (result.next()) {
+                return mapColumns(result);
+            } else {
+                return null;
+            }
+        }
+    }
 }
--- a/src/main/java/de/uapcore/lightpit/entities/Project.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/Project.java	Thu May 14 22:48:01 2020 +0200
@@ -28,8 +28,6 @@
  */
 package de.uapcore.lightpit.entities;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Objects;
 
 public class Project {
@@ -40,8 +38,6 @@
     private String repoUrl;
     private User owner;
 
-    private final List<Version> versions = new ArrayList<>();
-
     public Project(int id) {
         this.id = id;
     }
@@ -82,10 +78,6 @@
         this.owner = owner;
     }
 
-    public List<Version> getVersions() {
-        return versions;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
--- a/src/main/java/de/uapcore/lightpit/entities/User.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/User.java	Thu May 14 22:48:01 2020 +0200
@@ -80,6 +80,19 @@
         this.lastname = lastname;
     }
 
+    public String getDisplayname() {
+        StringBuilder dn = new StringBuilder();
+        dn.append(givenname);
+        dn.append(' ');
+        dn.append(lastname);
+        dn.append(' ');
+        if (mail != null && !mail.isBlank()) {
+            dn.append("<"+mail+">");
+        }
+        final var str = dn.toString().trim();
+        return str.isBlank() ? username : str;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
--- a/src/main/java/de/uapcore/lightpit/modules/HomeModule.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/HomeModule.java	Thu May 14 22:48:01 2020 +0200
@@ -31,6 +31,7 @@
 import de.uapcore.lightpit.*;
 
 import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServletRequest;
 
 /**
  * Entry point for the application.
@@ -47,7 +48,10 @@
 public final class HomeModule extends AbstractLightPITServlet {
 
     @RequestMapping(method = HttpMethod.GET)
-    public ResponseType handle() {
+    public ResponseType handle(HttpServletRequest req) {
+
+        setDynamicFragment(req, "home");
+        setStylesheet(req, "home");
 
         return ResponseType.HTML;
     }
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Thu May 14 22:48:01 2020 +0200
@@ -31,9 +31,13 @@
 
 import de.uapcore.lightpit.*;
 import de.uapcore.lightpit.dao.DataAccessObjects;
+import de.uapcore.lightpit.entities.Project;
+import de.uapcore.lightpit.entities.User;
 
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServletRequest;
+import java.sql.SQLException;
+import java.util.Optional;
 
 @LightPITModule(
         bundleBaseName = "localization.projects",
@@ -47,12 +51,59 @@
 public final class ProjectsModule extends AbstractLightPITServlet {
 
     @RequestMapping(method = HttpMethod.GET)
-    public ResponseType index(HttpServletRequest req, DataAccessObjects dao) {
+    public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
+        final var projectDao = dao.getProjectDao();
+
+        req.setAttribute("projects", projectDao.list());
+        setDynamicFragment(req, "projects");
+
+        return ResponseType.HTML;
+    }
+
+    @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
+    public ResponseType displayCreateForm(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
+        final var projectDao = dao.getProjectDao();
+
+        Optional<Integer> id = getParameter(req, Integer.class, "id");
+        if (id.isPresent()) {
+            req.setAttribute("project", Optional.ofNullable(projectDao.find(id.get())).orElse(new Project(-1)));
+        } else {
+            req.setAttribute("project", new Project(-1));
+        }
+
+        setDynamicFragment(req, "project-form");
 
         return ResponseType.HTML;
     }
 
-    @RequestMapping(method = HttpMethod.GET, requestPath = "versions", menuKey = "menu.versions")
+    @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
+    public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) {
+
+        Project project = new Project(-1);
+        try {
+            project = new Project(getParameter(req, Integer.class, "id").orElseThrow());
+            project.setName(getParameter(req, String.class, "name").orElseThrow());
+            getParameter(req, String.class, "description").ifPresent(project::setDescription);
+            getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl);
+            getParameter(req, Integer.class, "owner").map(
+                    ownerId -> ownerId >= 0 ? new User(ownerId) : null
+            ).ifPresent(project::setOwner);
+
+            dao.getProjectDao().saveOrUpdate(project);
+
+            setRedirectLocation(req, "./projects/");
+            setDynamicFragment(req, Constants.DYN_FRAGMENT_COMMIT_SUCCESSFUL);
+        } catch (NullPointerException | NumberFormatException | SQLException ex) {
+            // TODO: set request attribute with error text
+            req.setAttribute("project", project);
+            setDynamicFragment(req, "project-form");
+        }
+
+        return ResponseType.HTML;
+    }
+
+
+    @RequestMapping(requestPath = "versions", method = HttpMethod.GET, menuKey = "menu.versions")
     public ResponseType versions(HttpServletRequest req, DataAccessObjects dao) {
 
         return ResponseType.HTML;
--- a/src/main/resources/localization/home.properties	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/resources/localization/home.properties	Thu May 14 22:48:01 2020 +0200
@@ -24,3 +24,5 @@
 name = Home Page
 description = The default page that is displayed when visiting the site.
 menuLabel = Home
+
+version=LightPIT - Version 0.1 (Snapshot) 
\ No newline at end of file
--- a/src/main/resources/localization/home_de.properties	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/resources/localization/home_de.properties	Thu May 14 22:48:01 2020 +0200
@@ -24,3 +24,5 @@
 name = Startseite
 description = Die Seite, die dem Benutzer standardm\u00e4\u00dfig beim Besuch angezeigt wird.
 menuLabel = Startseite
+
+version=LightPIT - Version 0.1 (Entwicklungsversion) 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/resources/localization/lightpit.properties	Thu May 14 22:48:01 2020 +0200
@@ -0,0 +1,28 @@
+# Copyright 2018 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.
+
+button.okay=OK
+button.cancel=Cancel
+
+commit.success=Operation successful - you will be redirected in a second.
+commit.redirect-link=If redirection does not work, click the following link:
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/resources/localization/lightpit_de.properties	Thu May 14 22:48:01 2020 +0200
@@ -0,0 +1,28 @@
+# Copyright 2018 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.
+
+button.okay=OK
+button.cancel=Abbrechen
+
+commit.success=Operation erfolgreich - Sie werden jeden Moment weitergeleitet.
+commit.redirect-link=Falls die Weiterleitung nicht klappt, klicken Sie bitte hier:
--- a/src/main/resources/localization/projects.properties	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/resources/localization/projects.properties	Thu May 14 22:48:01 2020 +0200
@@ -20,7 +20,20 @@
 # 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.
+
 name=Project Management
 description=Allows the configuration of projects.
 menuLabel=Projects
+
 menu.versions=Versions
+
+button.create=New Project
+
+no-projects=Welcome to LightPIT. Start off by creating a new project!
+
+thead.name=Name
+thead.description=Description
+thead.repoUrl=Repository
+thead.owner=Project Lead
+
+placeholder.null-owner=Unassigned
--- a/src/main/resources/localization/projects_de.properties	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/resources/localization/projects_de.properties	Thu May 14 22:48:01 2020 +0200
@@ -20,7 +20,20 @@
 # 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.
+
 name=Projektverwaltung
 description=Erlaubt die Konfiguration von Projekten.
 menuLabel=Projekte
-menu.versions=Versionen
\ No newline at end of file
+
+menu.versions=Versionen
+
+button.create=Neues Projekt
+
+no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes!
+
+thead.name=Name
+thead.description=Beschreibung
+thead.repoUrl=Repository
+thead.owner=Projektleitung
+
+placeholder.null-owner=Nicht Zugewiesen
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/dynamic_fragments/commit-successful.jsp	Thu May 14 22:48:01 2020 +0200
@@ -0,0 +1,34 @@
+<%--
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 2018 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" %>
+<%@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="redirectLocation" value="${requestScope[Constants.REQ_ATTR_REDIRECT_LOCATION]}"/>
+
+<fmt:message bundle="${lightpit_bundle}" key="commit.success" />
+<fmt:message bundle="${lightpit_bundle}" key="commit.redirect-link" />
--- a/src/main/webapp/WEB-INF/dynamic_fragments/error.jsp	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/webapp/WEB-INF/dynamic_fragments/error.jsp	Thu May 14 22:48:01 2020 +0200
@@ -25,11 +25,13 @@
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 --%>
 <%@page pageEncoding="UTF-8" %>
+<%@page import="de.uapcore.lightpit.Constants" %>
 <%@page import="de.uapcore.lightpit.modules.ErrorModule" %>
 <%@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" %>
 
+<c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}" />
 <c:set scope="page" var="errorCode" value="${requestScope[ErrorModule.REQ_ATTR_ERROR_CODE]}"/>
 <c:set scope="page" var="returnLink" value="${requestScope[ErrorModule.REQ_ATTR_RETURN_LINK]}"/>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/dynamic_fragments/home.jsp	Thu May 14 22:48:01 2020 +0200
@@ -0,0 +1,32 @@
+<%--
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 2018 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" %>
+<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+
+<div class="smalltext">
+    <fmt:message key="version" />
+</div>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/dynamic_fragments/project-form.jsp	Thu May 14 22:48:01 2020 +0200
@@ -0,0 +1,75 @@
+<%--
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 2018 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" %>
+<%@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="moduleInfo" value="${requestScope[Constants.REQ_ATTR_MODULE_INFO]}"/>
+
+<jsp:useBean id="project" type="de.uapcore.lightpit.entities.Project" scope="request"/>
+
+<form action="./${moduleInfo.modulePath}/commit" method="post">
+    <table class="formtable" style="width: 80ch">
+        <colgroup>
+            <col>
+            <col style="width: 100%">
+        </colgroup>
+        <tbody>
+        <tr>
+            <th><fmt:message key="thead.name"/></th>
+            <td><input name="name" type="text" maxlength="20" required value="${project.name}"/> </td>
+        </tr>
+        <tr>
+            <th class="vtop"><fmt:message key="thead.description"/></th>
+            <td><input type="text" name="description" maxlength="200" value="${project.description}" /> </td>
+        </tr>
+        <tr>
+            <th><fmt:message key="thead.repoUrl"/></th>
+            <td><input name="repoUrl" type="url" maxlength="50" value="${project.repoUrl}" /> </td>
+        </tr>
+        <tr>
+            <th><fmt:message key="thead.owner"/></th>
+            <td>
+                <select name="owner">
+                    <option value="-1"><fmt:message key="placeholder.null-owner" /> </option>
+                    <!-- TODO: add user selection -->
+                </select>
+            </td>
+        </tr>
+        </tbody>
+        <tfoot>
+        <tr>
+            <td colspan="2">
+                <input type="hidden" name="id" value="${project.id}" />
+                <a href="./${moduleInfo.modulePath}" class="button"><fmt:message bundle="${lightpit_bundle}" key="button.cancel"/></a>
+                <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay" /></button>
+            </td>
+        </tr>
+        </tfoot>
+    </table>
+</form>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/dynamic_fragments/projects.jsp	Thu May 14 22:48:01 2020 +0200
@@ -0,0 +1,83 @@
+<%--
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 2018 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" %>
+<%@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="moduleInfo" value="${requestScope[Constants.REQ_ATTR_MODULE_INFO]}"/>
+
+<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request"/>
+
+<c:if test="${empty projects}">
+    <div class="info-box">
+    <fmt:message key="no-projects" />
+    </div>
+</c:if>
+
+<div id="tool-area">
+    <a href="./${moduleInfo.modulePath}/edit" class="button"><fmt:message key="button.create" /></a>
+</div>
+
+<c:if test="${not empty projects}">
+<table class="datatable medskip">
+    <colgroup>
+        <col>
+        <col style="width: 15%">
+        <col style="width: 35%">
+        <col style="width: 30%">
+        <col style="width: 20%">
+    </colgroup>
+    <thead>
+    <tr>
+        <th></th>
+        <th><fmt:message key="thead.name"/></th>
+        <th><fmt:message key="thead.description"/></th>
+        <th><fmt:message key="thead.repoUrl"/></th>
+        <th><fmt:message key="thead.owner"/></th>
+    </tr>
+    </thead>
+    <tbody>
+    <c:forEach var="project" items="${projects}">
+        <tr>
+            <td><a href="./${moduleInfo.modulePath}/edit?id=${project.id}">&#x270e;</a></td>
+            <td><c:out value="${project.name}"/></td>
+            <td><c:out value="${project.description}"/></td>
+            <td>
+                <c:if test="${not empty project.repoUrl}">
+                <a target="_blank" href="<c:out value="${project.repoUrl}"/>"><c:out value="${project.repoUrl}"/></a>
+                </c:if>
+            </td>
+            <td>
+            <c:if test="${not empty project.owner}"><c:out value="${project.owner.displayname}"/></c:if>
+            <c:if test="${empty project.owner}"><fmt:message key="placeholder.null-owner" /></c:if>
+            </td>
+        </tr>
+    </c:forEach>
+    </tbody>
+</table>
+</c:if>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/site.jsp	Thu May 14 22:48:01 2020 +0200
@@ -31,7 +31,7 @@
 <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
 
 <%-- Make the base href easily available at request scope --%>
-<c:set scope="request" var="baseHref" value="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/" />
+<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]}"/>
@@ -45,6 +45,9 @@
 <%-- Define an alias for the fragment name --%>
 <c:set scope="page" var="fragment" value="${requestScope[Constants.REQ_ATTR_FRAGMENT]}"/>
 
+<%-- Define an alias for the optional redirect location --%>
+<c:set scope="page" var="redirectLocation" value="${requestScope[Constants.REQ_ATTR_REDIRECT_LOCATION]}"/>
+
 <%-- Define an alias for the additional stylesheet --%>
 <c:set scope="page" var="extraCss" value="${requestScope[Constants.REQ_ATTR_STYLESHEET]}"/>
 
@@ -66,6 +69,9 @@
             </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}">
         <link rel="stylesheet" href="${extraCss}" type="text/css">
@@ -87,6 +93,7 @@
         <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>
--- a/src/main/webapp/lightpit.css	Wed May 13 21:46:26 2020 +0200
+++ b/src/main/webapp/lightpit.css	Thu May 14 22:48:01 2020 +0200
@@ -28,22 +28,17 @@
  */
 
 html {
-    background: #f8f8f8;
+    font-family: sans-serif;
+    background: white;
+    color: #1c204e;
+    margin: 0;
+    padding: 0;
 }
 
 body {
-    background: white;
-    font-family: serif;
-    
-    border-color: #505050;
-    border-style: solid;
-    border-width: 1pt;
-    
-    color: #1c202e;
-}
-
-h1, h2, h3, h4, #mainMenu, #subMenu {
-    font-family: sans-serif;
+    height: 100%;
+    margin: 0;
+    padding: 0;
 }
 
 a {
@@ -88,6 +83,33 @@
     padding: 1em;
 }
 
+button, a.button {
+    display: inline-block;
+    font-size: medium;
+    border-style: outset;
+    border-width: 2pt;
+    border-color: #6060cc;
+    color: inherit;
+    background: #f0f0f0;
+
+    padding: .25em .5em .25em .5em;
+    cursor: default;
+    text-decoration: none;
+}
+
+button:hover, a.button:hover {
+    background: #f0f0ff;
+}
+
+button[type=submit] {
+    background: #20a0ff;
+    color: white;
+}
+
+button[type=submit]:hover {
+    background: #1090cf;
+}
+
 th {
     text-align: left;
 }
@@ -101,6 +123,7 @@
 }
 
 table.datatable th {
+    white-space: nowrap;
     font-weight: bold;
     background: lightsteelblue;
 }
@@ -116,6 +139,35 @@
     background: lightblue;
 }
 
+table.formtable {
+    border-style: none;
+    border-collapse: separate;
+    border-spacing: 1em;
+}
+
+table.formtable th {
+    font-weight: bold;
+    text-align: left;
+    vertical-align: center;
+    white-space: nowrap;
+}
+
+table.formtable tbody td > * {
+    width: 100%;
+}
+
+table.formtable tfoot td {
+    text-align: right;
+}
+
+.fullwidth {
+    width: 100%;
+}
+
+.vtop {
+    vertical-align: top;
+}
+
 .hcenter {
     text-align: center;
 }
@@ -126,4 +178,16 @@
 
 .nowrap {
     white-space: nowrap;
+}
+
+.medskip {
+    margin-top: .5em;
+}
+
+.info-box {
+    margin: 2em;
+    border-style: dashed;
+    border-width: 1pt;
+    border-color: deepskyblue;
+    padding: 1em;
 }
\ No newline at end of file

mercurial