Tue, 03 Aug 2021 13:41:32 +0200
#21 adds input validation mechanism
Also upgrades to Kotlin 1.5.21
--- a/build.gradle.kts Tue Aug 03 12:22:10 2021 +0200 +++ b/build.gradle.kts Tue Aug 03 13:41:32 2021 +0200 @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.4.10" + kotlin("jvm") version "1.5.21" war } group = "de.uapcore"
--- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Tue Aug 03 12:22:10 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Tue Aug 03 13:41:32 2021 +0200 @@ -37,6 +37,10 @@ typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit typealias PathParameters = Map<String, String> +sealed interface ValidationResult<T> +class ValidationError<T>(val message: String): ValidationResult<T> +class ValidatedValue<T>(val result: T): ValidationResult<T> + class HttpRequest( val request: HttpServletRequest, val response: HttpServletResponse, @@ -155,6 +159,18 @@ fun param(name: String): String? = request.getParameter(name) fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray() + fun <T> param(name: String, validator: (String?) -> (ValidationResult<T>), errorMessages: MutableList<String>): T? { + return when (val result = validator(param(name))) { + is ValidationError -> { + errorMessages.add(i18n(result.message)) + null + } + is ValidatedValue -> { + result.result + } + } + } + private fun forward(jsp: String) { request.getRequestDispatcher(jspPath(jsp)).forward(request, response) }
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/UsersServlet.kt Tue Aug 03 12:22:10 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/UsersServlet.kt Tue Aug 03 13:41:32 2021 +0200 @@ -25,12 +25,9 @@ package de.uapcore.lightpit.servlet -import de.uapcore.lightpit.AbstractServlet -import de.uapcore.lightpit.HttpRequest -import de.uapcore.lightpit.LoggingTrait +import de.uapcore.lightpit.* import de.uapcore.lightpit.dao.DataAccessObject import de.uapcore.lightpit.entities.User -import de.uapcore.lightpit.logger import de.uapcore.lightpit.viewmodel.UserEditView import de.uapcore.lightpit.viewmodel.UsersView import javax.servlet.annotation.WebServlet @@ -48,21 +45,21 @@ private val list = "users" private val form = "user-form" - fun index(http: HttpRequest, dao: DataAccessObject) { + private fun index(http: HttpRequest, dao: DataAccessObject) { with(http) { view = UsersView(dao.listUsers()) render(list) } } - fun create(http: HttpRequest, dao: DataAccessObject) { + private fun create(http: HttpRequest, dao: DataAccessObject) { with(http) { view = UserEditView(User(-1)) render(form) } } - fun edit(http: HttpRequest, dao: DataAccessObject) { + private fun edit(http: HttpRequest, dao: DataAccessObject) { val id = http.pathParams["userid"]?.toIntOrNull() if (id == null) { http.response.sendError(404) @@ -79,7 +76,7 @@ } } - fun commit(http: HttpRequest, dao: DataAccessObject) { + private fun commit(http: HttpRequest, dao: DataAccessObject) { val id = http.param("userid")?.toIntOrNull() if (id == null) { http.response.sendError(400) @@ -88,25 +85,32 @@ val user = User(id) with(user) { - username = http.param("username") ?: "" givenname = http.param("givenname") lastname = http.param("lastname") mail = http.param("mail") } - if (dao.findUserByName(user.username) != null) { - with(http) { - view = UserEditView(user).apply { errorText = "validation.username.unique" } + if (user.id > 0) { + logger().info("Update user with id ${user.id}.") + dao.updateUser(user) + http.renderCommit("users/") + } else { + val errorMessages = mutableListOf<String>() + val username = http.param("username", { + if (it == null) ValidationError("validation.username.null") + else if (dao.findUserByName(it) != null) ValidationError("validation.username.unique") + else ValidatedValue(it) + }, errorMessages) + + if (username != null) { + logger().info("Insert user ${username}.") + user.username = username + dao.insertUser(user) + http.renderCommit("users/") + } else { + http.view = UserEditView(user).apply { this.errorMessages = errorMessages } + http.render(form) } } - - if (user.id > 0) { - logger().info("Update user ${user.username} with id ${user.id}.") - dao.updateUser(user) - } else { - logger().info("Insert user ${user.username}.") - dao.insertUser(user) - } - http.renderCommit("users/") } } \ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt Tue Aug 03 12:22:10 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt Tue Aug 03 13:41:32 2021 +0200 @@ -35,7 +35,7 @@ /** * The color representation with the leading hash symbol. */ - val color: String = (if (arg.startsWith("#")) arg else "#$arg").toUpperCase() + val color: String = (if (arg.startsWith("#")) arg else "#$arg").uppercase() /** * The hex representation without the leading hash symbol.
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/View.kt Tue Aug 03 12:22:10 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/View.kt Tue Aug 03 13:41:32 2021 +0200 @@ -27,5 +27,5 @@ abstract class View abstract class EditView : View() { - var errorText: String? = null + var errorMessages: List<String> = emptyList() }
--- a/src/main/resources/localization/strings.properties Tue Aug 03 12:22:10 2021 +0200 +++ b/src/main/resources/localization/strings.properties Tue Aug 03 13:41:32 2021 +0200 @@ -128,6 +128,7 @@ user.lastname=Last Name user.mail=E-Mail username=User Name +validation.username.null=Username is mandatory. validation.username.unique=Username is already taken. version.latest=Latest Version version.next=Next Version
--- a/src/main/resources/localization/strings_de.properties Tue Aug 03 12:22:10 2021 +0200 +++ b/src/main/resources/localization/strings_de.properties Tue Aug 03 13:41:32 2021 +0200 @@ -127,6 +127,7 @@ user.lastname=Nachname user.mail=E-Mail username=Benutzername +validation.username.null=Benutzername ist ein Pflichtfeld. validation.username.unique=Der Benutzername wird bereits verwendet. version.latest=Neuste Version version.next=N\u00e4chste Version
--- a/src/main/webapp/WEB-INF/jsp/user-form.jsp Tue Aug 03 12:22:10 2021 +0200 +++ b/src/main/webapp/WEB-INF/jsp/user-form.jsp Tue Aug 03 13:41:32 2021 +0200 @@ -38,34 +38,30 @@ <col style="width: 50ch"> </colgroup> <tbody> + <c:if test="${not empty viewmodel.errorMessages}"> + <tr> + <td colspan="2"><%@include file="../jspf/error-messages.jspf" %></td> + </tr> + </c:if> <tr> <th><fmt:message key="username"/></th> - <td><input name="username" type="text" maxlength="50" required value="<c:out value="${user.username}"/>" + <td><input name="username" type="text" maxlength="200" required value="<c:out value="${user.username}"/>" <c:if test="${user.id ge 0}">readonly</c:if> /></td> </tr> <tr> <th><fmt:message key="user.givenname"/></th> - <td><input name="givenname" type="text" maxlength="50" value="<c:out value="${user.givenname}"/>" /></td> + <td><input name="givenname" type="text" maxlength="200" value="<c:out value="${user.givenname}"/>" /></td> </tr> <tr> <th><fmt:message key="user.lastname"/></th> - <td><input name="lastname" type="text" maxlength="50" value="<c:out value="${user.lastname}"/>" /></td> + <td><input name="lastname" type="text" maxlength="200" value="<c:out value="${user.lastname}"/>" /></td> </tr> <tr> <th><fmt:message key="user.mail"/></th> - <td><input name="mail" type="email" maxlength="50" value="<c:out value="${user.mail}"/>" /></td> + <td><input name="mail" type="email" maxlength="200" value="<c:out value="${user.mail}"/>" /></td> </tr> </tbody> <tfoot> - <c:if test="${not empty viewmodel.errorText}"> - <tr> - <td colspan="2"> - <div class="error-box"> - <fmt:message key="${viewmodel.errorText}"/> - </div> - </td> - </tr> - </c:if> <tr> <td colspan="2"> <input type="hidden" name="userid" value="${user.id}"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jspf/error-messages.jspf Tue Aug 03 13:41:32 2021 +0200 @@ -0,0 +1,34 @@ +<%-- + ~ Copyright 2021 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" %> + +<div class="error-box hcenter"> + <c:forEach var="errorMessage" items="${viewmodel.errorMessages}"> + <div> + <c:out value="${errorMessage}"/> + </div> + </c:forEach> +</div>
--- a/src/main/webapp/lightpit.css Tue Aug 03 12:22:10 2021 +0200 +++ b/src/main/webapp/lightpit.css Tue Aug 03 13:41:32 2021 +0200 @@ -198,6 +198,7 @@ table.formtable tbody td > * { width: 100%; + margin: 0; box-sizing: border-box; } @@ -238,7 +239,7 @@ } .info-box, .error-box, .warn-box { - margin: 2em; + margin: 1.5em; border-style: dashed; border-width: thin; border-color: deepskyblue; @@ -246,11 +247,15 @@ } .error-box { + border-style: outset; border-color: red; + background: lightcoral; } .warn-box { + border-style: outset; border-color: gold; + background: lightgoldenrodyellow; } .table {