From bf91733b92f3aa708cf97664c7502c5edd32d2f4 Mon Sep 17 00:00:00 2001 From: Miguel Reboiro-Jato Date: Sun, 14 Feb 2016 19:19:39 +0100 Subject: [PATCH] Refactorizes the whole project for a simpler testing and execution A big refactorization has been done in this commit, focused on simplifying the testing and execution process. The Spring-DbUnit library has been added to allow the use of DbUnit through annotations in the test classes. This change has been complemented with two new test listeners that allow the creation of custom initial contexts and the use of custom SQL scripts to create and destroy the database tables before and after the test execution, respectively. In addition, all the tests are executed now using a HSQLDB database. The datasource configuration has been pulled out of the project, and now it must be provided by the container. The sample classes have been reviewed, refactorized and documented with JavaDoc. The POM file has been reworked to ease test and application execution using maven. The new execution modes are: - The default execution includes unit, integration and acceptance (with Selenium) test execution. - No acceptance tests: mvn -P -acceptance-tests-cargo - Run server for manual acceptance tests: mvn -Dcargo.tomcat.start.skip=true -Dcargo.tomcat.run.skip=false -DskipTests=true pre-integration-test - Run server with MySQL database: mvn -P run-tomcat-mysql,-acceptance-tests-cargo -DskipTests=true package cargo:run --- .gitignore | 16 +- README.md | 21 +- .../resources => db}/mysql-with-inserts.sql | 4 +- {src/main/resources => db}/mysql.sql | 2 +- pom.xml | 483 ++++++++++++------ .../uvigo/esei/daa/DAAExampleApplication.java | 16 +- .../java/es/uvigo/esei/daa/LoginFilter.java | 98 +++- src/main/java/es/uvigo/esei/daa/dao/DAO.java | 24 +- .../es/uvigo/esei/daa/dao/DAOException.java | 58 +++ .../java/es/uvigo/esei/daa/dao/PeopleDAO.java | 148 ++++-- .../java/es/uvigo/esei/daa/dao/UsersDAO.java | 77 +-- .../es/uvigo/esei/daa/entities/Person.java | 69 ++- .../uvigo/esei/daa/rest/PeopleResource.java | 170 ++++-- src/main/webapp/META-INF/context.xml | 30 -- src/main/webapp/WEB-INF/web.xml | 21 +- src/main/webapp/index.html | 12 +- src/main/webapp/js/view/people.js | 4 + src/main/webapp/main.html | 2 +- .../uvigo/esei/daa/DatabaseQueryUnitTest.java | 7 - .../java/es/uvigo/esei/daa/TestUtils.java | 69 +-- .../es/uvigo/esei/daa/dao/PeopleDAOTest.java | 71 +-- .../uvigo/esei/daa/dao/PeopleDAOUnitTest.java | 25 +- .../listeners/ApplicationContextBinding.java | 18 + .../listeners/ApplicationContextBindings.java | 12 + ...ntextJndiBindingTestExecutionListener.java | 44 ++ .../esei/daa/listeners/DbManagement.java | 14 + .../daa/listeners/DbManagementAction.java | 5 + .../DbManagementTestExecutionListener.java | 113 ++++ .../esei/daa/rest/PeopleResourceTest.java | 89 ++-- .../esei/daa/rest/PeopleResourceUnitTest.java | 17 +- .../esei/daa/suites/AcceptanceTestSuite.java | 3 + .../esei/daa/suites/IntegrationTestSuite.java | 4 +- .../es/uvigo/esei/daa/web/PeopleWebTest.java | 142 +++-- .../es/uvigo/esei/daa/web/pages/MainPage.java | 224 ++++++++ src/test/resources/contexts/hsql-context.xml | 26 + src/test/resources/contexts/mem-context.xml | 26 + src/test/resources/datasets/dataset-add.xml | 18 + .../resources/datasets/dataset-delete.xml | 16 + .../resources/datasets/dataset-modify.xml | 17 + src/test/resources/datasets/dataset.dtd | 13 + src/test/resources/datasets/dataset.xml | 17 + src/test/resources/db/hsqldb-drop.sql | 2 + src/test/resources/db/hsqldb.sql | 12 + src/test/resources/mysql-tests-clear.sql | 4 - src/test/resources/mysql-tests.sql | 28 - src/test/resources/mysql.sql | 16 - 46 files changed, 1616 insertions(+), 691 deletions(-) rename {src/main/resources => db}/mysql-with-inserts.sql (85%) rename {src/main/resources => db}/mysql.sql (88%) delete mode 100644 src/main/webapp/META-INF/context.xml create mode 100644 src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextBinding.java create mode 100644 src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextBindings.java create mode 100644 src/test/java/es/uvigo/esei/daa/listeners/ApplicationContextJndiBindingTestExecutionListener.java create mode 100644 src/test/java/es/uvigo/esei/daa/listeners/DbManagement.java create mode 100644 src/test/java/es/uvigo/esei/daa/listeners/DbManagementAction.java create mode 100644 src/test/java/es/uvigo/esei/daa/listeners/DbManagementTestExecutionListener.java create mode 100644 src/test/java/es/uvigo/esei/daa/web/pages/MainPage.java create mode 100644 src/test/resources/contexts/hsql-context.xml create mode 100644 src/test/resources/contexts/mem-context.xml create mode 100644 src/test/resources/datasets/dataset-add.xml create mode 100644 src/test/resources/datasets/dataset-delete.xml create mode 100644 src/test/resources/datasets/dataset-modify.xml create mode 100644 src/test/resources/datasets/dataset.dtd create mode 100644 src/test/resources/datasets/dataset.xml create mode 100644 src/test/resources/db/hsqldb-drop.sql create mode 100644 src/test/resources/db/hsqldb.sql delete mode 100644 src/test/resources/mysql-tests-clear.sql delete mode 100644 src/test/resources/mysql-tests.sql delete mode 100644 src/test/resources/mysql.sql diff --git a/.gitignore b/.gitignore index 4cf3d32..4b3c842 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,16 @@ -/bin -/target -/assembly +# General +/bak + +# Eclipse .project .classpath .settings + +# Maven +/bin +/target +/assembly + +# Testing +/servers +C:\\nppdf32Log\\debuglog.txt diff --git a/README.md b/README.md index 10438ca..23fcee5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,23 @@ DAAExample ========== -Example application for the DAA Subject +Aplicación y arquitectura de ejemplo para la asignatura Desarrollo Ágil de +Aplicaciones del Grado en Ingeniería Informática de la Escuela Superior de +Ingeniería Informática de la Universidad de Vigo. + +## Ejecución con Maven +La configuración de Maven ha sido preparada para permitir varios tipos de +ejecución. En concreto: +* La ejecución por defecto (p.ej. `mvn install`) incluye los tests de +unidad, integración y aceptación (con Selenium). +* Si no se desea ejecutar los tests de aceptación debe desactivarse el perfil +`acceptance-tests-cargo`. Por ejemplo: `mvn -P -acceptance-tests-cargo install`. +* Si se desea arrancar el servidor para ejecutar los tests de aceptación +manualmente (se usará una base de datos HSQL), se debe ejecutar el comando: +`mvn -Dcargo.tomcat.start.skip=true -Dcargo.tomcat.run.skip=false +-DskipTests=true pre-integration-test` +* Si se desea arrancar el servidor con la base de datos MySQL, debe utilizarse +el comando: `mvn -P run-tomcat-mysql,-acceptance-tests-cargo cargo:run`. Es +necesario que el proyecto se haya empaquetado antes (p.ej. `mvn package`). En el +directorio `db` del proyecto se pueden encontrar los scripts necesarios para +crear la base de datos en MySQL. diff --git a/src/main/resources/mysql-with-inserts.sql b/db/mysql-with-inserts.sql similarity index 85% rename from src/main/resources/mysql-with-inserts.sql rename to db/mysql-with-inserts.sql index 2210aa5..7c0a69c 100644 --- a/src/main/resources/mysql-with-inserts.sql +++ b/db/mysql-with-inserts.sql @@ -9,7 +9,7 @@ CREATE TABLE `daaexample`.`people` ( CREATE TABLE `daaexample`.`users` ( `login` varchar(100) NOT NULL, - `password` varbinary(64) DEFAULT NULL, + `password` varchar(64) DEFAULT NULL, PRIMARY KEY (`login`) ); @@ -24,4 +24,4 @@ INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'María','Nu INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Alba','Fernández'); INSERT INTO `daaexample`.`people` (`id`,`name`,`surname`) VALUES (0,'Asunción','Jiménez'); -INSERT INTO `daaexample`.`users` (`login`,`password`) VALUES ('mrjato', '59189332a4abf8ddf66fde068cad09eb563b4bd974f7663d97ff6852a7910a73'); +INSERT INTO `daaexample`.`users` (`login`,`password`) VALUES ('admin', '0b893644f3b2097d004c58d585e784ac92dd1356d25158a298573ad54ab2d15d'); diff --git a/src/main/resources/mysql.sql b/db/mysql.sql similarity index 88% rename from src/main/resources/mysql.sql rename to db/mysql.sql index 88e93d1..a7d1c3e 100644 --- a/src/main/resources/mysql.sql +++ b/db/mysql.sql @@ -9,7 +9,7 @@ CREATE TABLE `daaexample`.`people` ( CREATE TABLE `daaexample`.`users` ( `login` varchar(100) NOT NULL, - `password` varbinary(64) DEFAULT NULL, + `password` varchar(64) DEFAULT NULL, PRIMARY KEY (`login`) ); diff --git a/pom.xml b/pom.xml index 062bb86..2105a51 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ es.uvigo.esei.daa example war - 0.0.1-SNAPSHOT + 0.1.0-SNAPSHOT DAA Example @@ -16,35 +16,40 @@ - + 1.8 1.8 UTF-8 + ${project.basedir}/servers + false + true - - 2.15 - 5.1.34 - 1.4 - 3.0.1 + + 2.22.1 + 3.1.0 + 2.1.1 + 1.7.16 + + 4.12 - 2.44.0 - 4.1.4.RELEASE - 3.3.1 + 3.4 + 2.49.1 + 4.2.4.RELEASE + 2.5.1 + 1.2.1 + 2.3.3 + 5.1.38 - 0.7.2.201409121644 - 2.18.1 + 2.19.1 + 2.19.1 2.6 2.2 + 0.7.5.201505241946 + 1.4.18 - - org.glassfish.jersey.containers - jersey-container-servlet - ${jersey.version} - - javax.servlet javax.servlet-api @@ -52,6 +57,12 @@ provided + + org.glassfish.jersey.containers + jersey-container-servlet + ${jersey.version} + + org.glassfish.jersey.media jersey-media-json-jackson @@ -59,9 +70,9 @@ - mysql - mysql-connector-java - ${mysql.version} + org.slf4j + slf4j-jdk14 + ${slf4j-jdk14.version} @@ -72,6 +83,13 @@ test + + org.easymock + easymock + ${easymock.version} + test + + org.glassfish.jersey.test-framework.providers jersey-test-framework-provider-grizzly2 @@ -80,8 +98,8 @@ - commons-dbcp - commons-dbcp + org.apache.commons + commons-dbcp2 ${commons.dbcp.version} test @@ -100,84 +118,62 @@ test + + org.springframework + spring-context + ${spring.test.version} + test + + + + org.springframework + spring-jdbc + ${spring.test.version} + test + + - org.easymock - easymock - ${easymock.version} + org.dbunit + dbunit + ${dbunit.version} + jar + test + + + + com.github.springtestdbunit + spring-test-dbunit + ${spring-test-dbunit.version} test - - - - - org.apache.maven.plugins - maven-surefire-report-plugin - ${surefire.version} - - - integration-tests - - failsafe-report-only - - - - - - + + org.hsqldb + hsqldb + ${hsqldb.version} + test + + + + mysql + mysql-connector-java + ${mysql.version} + test + + DAAExample + - maven-war-plugin org.apache.maven.plugins ${maven.war.plugin.version} - DAAExample + ${project.finalName} - - - org.jacoco - jacoco-maven-plugin - ${jacoco.version} - - - default-prepare-agent - - prepare-agent - - - - default-prepare-agent-integration - - prepare-agent-integration - - - - coverage-report - test - - report - - - - coverage-report-integration - integration-test - - report-integration - - - - default-check - - check - - - - org.apache.maven.plugins @@ -189,15 +185,14 @@ - + org.apache.maven.plugins maven-failsafe-plugin - ${surefire.version} + ${failsafe.version} **/IntegrationTestSuite.java - **/AcceptanceTestSuite.java @@ -224,80 +219,276 @@ report-only - - integration-test-report - integration-test - - report-only - failsafe-report-only - - - org.apache.tomcat.maven - tomcat7-maven-plugin - ${tomcat.maven.plugin.version} + org.jacoco + jacoco-maven-plugin + ${jacoco.version} - 9080 - /DAAExample - src/test/webapp/META-INF/context.xml - false + - start-tomcat7 - pre-integration-test + default-prepare-agent - run + prepare-agent - - true - - stop-tomcat7 - post-integration-test + coverage-report + test - shutdown + report + + + + default-check + + check - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - org.jacoco - - jacoco-maven-plugin - - - [0.7.2.201409121644,) - - - prepare-agent - - - - - - - - - - - - + + + + acceptance-tests-cargo + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${failsafe.version} + + + **/IntegrationTestSuite.java + **/AcceptanceTestSuite.java + + + + + default-integration-tests + + integration-test + verify + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + ${surefire.version} + + + + integration-test-report + integration-test + + report-only + failsafe-report-only + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-agent + pre-integration-test + + prepare-agent + + + jacoco.agent.itArgLine + + + + jacoco-report + post-integration-test + + dump + report + + + ${project.reporting.outputDirectory}/jacoco-it + + + + + + + + fr.avianey.mojo + hsqldb-maven-plugin + 1.0.0 + + + + org.hsqldb.jdbc.JDBCDriver + mem:daatestdb +
localhost
+ daatestdb + sa + +
+ + + + + start-hsqldb + pre-integration-test + + start + + + + stop-hsqldb + post-integration-test + + stop + + + +
+ + + org.codehaus.cargo + cargo-maven2-plugin + ${cargo-maven2-plugin.version} + + + tomcat8x + + http://ftp.cixug.es/apache/tomcat/tomcat-8/v8.0.32/bin/apache-tomcat-8.0.32.zip + ${project.servers.directory}/downloads + ${project.servers.directory}/extracts + + + + org.hsqldb + hsqldb + + + + + + ${project.build.directory}/catalina-base + + ${jacoco.agent.itArgLine},output=tcpserver,port=6300 -Drunmode=TEST + 9080 + + cargo.datasource.jndi=jdbc/daaexample| + cargo.datasource.driver=org.hsqldb.jdbc.JDBCDriver| + cargo.datasource.url=jdbc:hsqldb:hsql://localhost/daatestdb| + cargo.datasource.username=sa| + cargo.datasource.password=| + cargo.datasource.maxActive=8| + cargo.datasource.maxIdle=4| + cargo.datasource.maxWait=10000 + + + + + + + start-tomcat + pre-integration-test + + start + + + ${cargo.tomcat.start.skip} + + + + run-tomcat + pre-integration-test + + run + + + ${cargo.tomcat.run.skip} + + + + + stop-tomcat + post-integration-test + + stop + + + + +
+
+
+ + + run-tomcat-mysql + + false + + + + + + org.codehaus.cargo + cargo-maven2-plugin + ${cargo-maven2-plugin.version} + + + tomcat8x + + http://ftp.cixug.es/apache/tomcat/tomcat-8/v8.0.32/bin/apache-tomcat-8.0.32.zip + ${project.servers.directory}/downloads + ${project.servers.directory}/extracts + + + + mysql + mysql-connector-java + + + + + + ${project.build.directory}/catalina-base + + 9080 + + cargo.datasource.jndi=jdbc/daaexample| + cargo.datasource.driver=com.mysql.jdbc.Driver| + cargo.datasource.url=jdbc:mysql://localhost/daaexample| + cargo.datasource.username=daa| + cargo.datasource.password=daa| + cargo.datasource.maxActive=8| + cargo.datasource.maxIdle=4| + cargo.datasource.maxWait=10000 + + + + + + + + +
diff --git a/src/main/java/es/uvigo/esei/daa/DAAExampleApplication.java b/src/main/java/es/uvigo/esei/daa/DAAExampleApplication.java index 7fbbbcb..9ee0e9e 100644 --- a/src/main/java/es/uvigo/esei/daa/DAAExampleApplication.java +++ b/src/main/java/es/uvigo/esei/daa/DAAExampleApplication.java @@ -1,23 +1,29 @@ package es.uvigo.esei.daa; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; import es.uvigo.esei.daa.rest.PeopleResource; +/** + * Configuration of the REST application. This class includes the resources and + * configuration parameter used in the REST API of the application. + * + * @author Miguel Reboiro Jato + * + */ @ApplicationPath("/rest/*") public class DAAExampleApplication extends Application { @Override public Set> getClasses() { - return new HashSet<>(Arrays.asList( - PeopleResource.class - )); + return Stream.of(PeopleResource.class) + .collect(Collectors.toSet()); } @Override diff --git a/src/main/java/es/uvigo/esei/daa/LoginFilter.java b/src/main/java/es/uvigo/esei/daa/LoginFilter.java index 8e3cd50..4860187 100644 --- a/src/main/java/es/uvigo/esei/daa/LoginFilter.java +++ b/src/main/java/es/uvigo/esei/daa/LoginFilter.java @@ -1,6 +1,9 @@ package es.uvigo.esei.daa; +import static java.util.Objects.requireNonNull; + import java.io.IOException; +import java.util.Base64; import java.util.Optional; import javax.servlet.Filter; @@ -17,8 +20,22 @@ import javax.servlet.http.HttpServletResponse; import es.uvigo.esei.daa.dao.DAOException; import es.uvigo.esei.daa.dao.UsersDAO; +/** + * Security filter that implements a login protocol based on the HTTP Basic + * Authentication protocol. In this case, the login and password can be provided + * as plain request parameters or as a cookie named "token" that should contain + * both values in the same format as HTTP Basic Authentication + * ({@code base64(login + ":" + password)}). + * + * @author Miguel Reboiro Jato + * + */ @WebFilter(urlPatterns = { "/*", "/logout" }) public class LoginFilter implements Filter { + private static final String REST_PATH = "/rest"; + private static final String INDEX_PATH = "/index.html"; + private static final String LOGOUT_PATH = "/logout"; + @Override public void doFilter( ServletRequest request, @@ -47,17 +64,25 @@ public class LoginFilter implements Filter { httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } + + @Override + public void init(FilterConfig config) throws ServletException { + } + + @Override + public void destroy() { + } private boolean isLogoutPath(HttpServletRequest request) { - return request.getServletPath().equals("/logout"); + return request.getServletPath().equals(LOGOUT_PATH); } private boolean isIndexPath(HttpServletRequest request) { - return request.getServletPath().equals("/index.html"); + return request.getServletPath().equals(INDEX_PATH); } private boolean isRestPath(HttpServletRequest request) { - return request.getServletPath().startsWith("/rest"); + return request.getServletPath().startsWith(REST_PATH); } private void redirectToIndex( @@ -92,14 +117,15 @@ public class LoginFilter implements Filter { final String password = request.getParameter("password"); if (login != null && password != null) { - final String token = new UsersDAO().checkLogin(login, password); - - if (token == null) { - return false; - } else { - response.addCookie(new Cookie("token", token)); + final UsersDAO dao = new UsersDAO(); + if (dao.checkLogin(login, password)) { + final Credentials credentials = new Credentials(login, password); + + response.addCookie(new Cookie("token", credentials.toToken())); return true; + } else { + return false; } } else { return false; @@ -113,20 +139,56 @@ public class LoginFilter implements Filter { for (Cookie cookie : cookies) { if ("token".equals(cookie.getName())) { - final String token = new UsersDAO().checkToken(cookie.getValue()); + final Credentials credentials = new Credentials(cookie.getValue()); - return token != null; + final UsersDAO dao = new UsersDAO(); + + return dao.checkLogin(credentials.getLogin(), credentials.getPassword()); } } return false; } - - @Override - public void init(FilterConfig config) throws ServletException { - } - - @Override - public void destroy() { + + private static class Credentials { + private final String login; + private final String password; + + public Credentials(String token) { + final String decodedToken = decodeBase64(token); + final int colonIndex = decodedToken.indexOf(':'); + + if (colonIndex < 0 || colonIndex == decodedToken.length()-1) { + throw new IllegalArgumentException("Invalid token"); + } + + this.login = decodedToken.substring(0, colonIndex); + this.password = decodedToken.substring(colonIndex + 1); + } + + public Credentials(String login, String password) { + this.login = requireNonNull(login, "Login can't be null"); + this.password = requireNonNull(password, "Password can't be null"); + } + + public String getLogin() { + return login; + } + + public String getPassword() { + return password; + } + + public String toToken() { + return encodeBase64(this.login + ":" + this.password); + } + + private final static String decodeBase64(String text) { + return new String(Base64.getDecoder().decode(text.getBytes())); + } + + private final static String encodeBase64(String text) { + return Base64.getEncoder().encodeToString(text.getBytes()); + } } } diff --git a/src/main/java/es/uvigo/esei/daa/dao/DAO.java b/src/main/java/es/uvigo/esei/daa/dao/DAO.java index 0c2320d..1a83eee 100644 --- a/src/main/java/es/uvigo/esei/daa/dao/DAO.java +++ b/src/main/java/es/uvigo/esei/daa/dao/DAO.java @@ -10,25 +10,41 @@ import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; +/** + * Simple base class for DAO (Data Access Object) classes. This super-class is + * responsible for providing a {@link java.sql.Connection} to its sub-classes. + * + * @author Miguel Reboiro Jato + * + */ public abstract class DAO { - private final static Logger LOG = Logger.getLogger("DAO"); + private final static Logger LOG = Logger.getLogger(DAO.class.getName()); private final static String JNDI_NAME = "java:/comp/env/jdbc/daaexample"; private DataSource dataSource; + /** + * Constructs a new instance of {@link DAO}. + */ public DAO() { Context initContext; try { initContext = new InitialContext(); - this.dataSource = (DataSource) initContext.lookup( - System.getProperty("db.jndi", JNDI_NAME) - ); + + this.dataSource = (DataSource) initContext.lookup(JNDI_NAME); } catch (NamingException e) { LOG.log(Level.SEVERE, "Error initializing DAO", e); throw new RuntimeException(e); } } + /** + * Returns an open {@link java.sql.Connection}. + * + * @return an open {@link java.sql.Connection}. + * @throws SQLException if an error happens while establishing the + * connection with the database. + */ protected Connection getConnection() throws SQLException { return this.dataSource.getConnection(); } diff --git a/src/main/java/es/uvigo/esei/daa/dao/DAOException.java b/src/main/java/es/uvigo/esei/daa/dao/DAOException.java index fda528b..ffd4233 100644 --- a/src/main/java/es/uvigo/esei/daa/dao/DAOException.java +++ b/src/main/java/es/uvigo/esei/daa/dao/DAOException.java @@ -1,23 +1,81 @@ package es.uvigo.esei.daa.dao; +/** + * A general exception class for the DAO layer. + * + * @author Miguel Reboiro Jato + */ public class DAOException extends Exception { private static final long serialVersionUID = 1L; + /** + * Constructs a new instance of {@link DAOException} with {@code null} as + * its detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * + */ public DAOException() { } + /** + * Constructs a new instance of {@link DAOException} with the specified + * detail message. The cause is not initialized, and may subsequently be + * initialized by a call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for later + * retrieval by the {@link #getMessage()} method. + */ public DAOException(String message) { super(message); } + /** + * Constructs a new instance of {@link DAOException} with the specified + * cause and a detail message of + * {@code (cause==null ? null : cause.toString())} (which typically contains + * the class and detail message of {@code cause}). This constructor is + * useful for exceptions that are little more than wrappers for other + * throwables (for example, {@link java.security.PrivilegedActionException}). + * + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is permitted, and + * indicates that the cause is nonexistent or unknown.) + */ public DAOException(Throwable cause) { super(cause); } + /** + * Constructs a new instance of {@link DAOException} with the specified + * detail message and cause. + * + *

Note that the detail message associated with {@code cause} is + * not automatically incorporated in this exception's detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). A {@code null} value is permitted, and + * indicates that the cause is nonexistent or unknown. + */ public DAOException(String message, Throwable cause) { super(message, cause); } + /** + * Constructs a new instance of {@link DAOException} with the specified + * detail message, cause, suppression enabled or disabled, and writable + * stack trace enabled or disabled. + * + * @param message the detail message. + * @param cause the cause. A {@code null} value is permitted, and indicates + * that the cause is nonexistent or unknown. + * @param enableSuppression whether or not suppression is enabled or + * disabled. + * @param writableStackTrace whether or not the stack trace should be + * writable. + */ public DAOException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); diff --git a/src/main/java/es/uvigo/esei/daa/dao/PeopleDAO.java b/src/main/java/es/uvigo/esei/daa/dao/PeopleDAO.java index dfbbb13..1d99edb 100644 --- a/src/main/java/es/uvigo/esei/daa/dao/PeopleDAO.java +++ b/src/main/java/es/uvigo/esei/daa/dao/PeopleDAO.java @@ -12,9 +12,24 @@ import java.util.logging.Logger; import es.uvigo.esei.daa.entities.Person; +/** + * DAO class for the {@link Person} entities. + * + * @author Miguel Reboiro Jato + * + */ public class PeopleDAO extends DAO { - private final static Logger LOG = Logger.getLogger("PeopleDAO"); + private final static Logger LOG = Logger.getLogger(PeopleDAO.class.getName()); + /** + * Returns a person stored persisted in the system. + * + * @param id identifier of the person. + * @return a person with the provided identifier. + * @throws DAOException if an error happens while retrieving the person. + * @throws IllegalArgumentException if the provided id does not corresponds + * with any persisted person. + */ public Person get(int id) throws DAOException, IllegalArgumentException { try (final Connection conn = this.getConnection()) { @@ -25,11 +40,7 @@ public class PeopleDAO extends DAO { try (final ResultSet result = statement.executeQuery()) { if (result.next()) { - return new Person( - result.getInt("id"), - result.getString("name"), - result.getString("surname") - ); + return rowToEntity(result); } else { throw new IllegalArgumentException("Invalid id"); } @@ -41,6 +52,12 @@ public class PeopleDAO extends DAO { } } + /** + * Returns a list with all the people persisted in the system. + * + * @return a list with all the people persisted in the system. + * @throws DAOException if an error happens while retrieving the people. + */ public List list() throws DAOException { try (final Connection conn = this.getConnection()) { final String query = "SELECT * FROM people"; @@ -50,11 +67,7 @@ public class PeopleDAO extends DAO { final List people = new LinkedList<>(); while (result.next()) { - people.add(new Person( - result.getInt("id"), - result.getString("name"), - result.getString("surname") - )); + people.add(rowToEntity(result)); } return people; @@ -66,41 +79,73 @@ public class PeopleDAO extends DAO { } } - public void delete(int id) + /** + * Persists a new person in the system. An identifier will be assigned + * automatically to the new person. + * + * @param name name of the new person. Can't be {@code null}. + * @param surname surname of the new person. Can't be {@code null}. + * @return a {@link Person} entity representing the persisted person. + * @throws DAOException if an error happens while persisting the new person. + * @throws IllegalArgumentException if the name or surname are {@code null}. + */ + public Person add(String name, String surname) throws DAOException, IllegalArgumentException { - try (final Connection conn = this.getConnection()) { - final String query = "DELETE FROM people WHERE id=?"; + if (name == null || surname == null) { + throw new IllegalArgumentException("name and surname can't be null"); + } + + try (Connection conn = this.getConnection()) { + final String query = "INSERT INTO people VALUES(null, ?, ?)"; - try (final PreparedStatement statement = conn.prepareStatement(query)) { - statement.setInt(1, id); + try (PreparedStatement statement = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, name); + statement.setString(2, surname); - if (statement.executeUpdate() != 1) { - throw new IllegalArgumentException("Invalid id"); + if (statement.executeUpdate() == 1) { + try (ResultSet resultKeys = statement.getGeneratedKeys()) { + if (resultKeys.next()) { + return new Person(resultKeys.getInt(1), name, surname); + } else { + LOG.log(Level.SEVERE, "Error retrieving inserted id"); + throw new SQLException("Error retrieving inserted id"); + } + } + } else { + LOG.log(Level.SEVERE, "Error inserting value"); + throw new SQLException("Error inserting value"); } } } catch (SQLException e) { - LOG.log(Level.SEVERE, "Error deleting a person", e); + LOG.log(Level.SEVERE, "Error adding a person", e); throw new DAOException(e); } } - public Person modify(int id, String name, String surname) + /** + * Modifies a person previously persisted in the system. The person will be + * retrieved by the provided id and its current name and surname will be + * replaced with the provided. + * + * @param person a {@link Person} entity with the new data. + * @throws DAOException if an error happens while modifying the new person. + * @throws IllegalArgumentException if the person is {@code null}. + */ + public void modify(Person person) throws DAOException, IllegalArgumentException { - if (name == null || surname == null) { - throw new IllegalArgumentException("name and surname can't be null"); + if (person == null) { + throw new IllegalArgumentException("person can't be null"); } try (Connection conn = this.getConnection()) { final String query = "UPDATE people SET name=?, surname=? WHERE id=?"; try (PreparedStatement statement = conn.prepareStatement(query)) { - statement.setString(1, name); - statement.setString(2, surname); - statement.setInt(3, id); + statement.setString(1, person.getName()); + statement.setString(2, person.getSurname()); + statement.setInt(3, person.getId()); - if (statement.executeUpdate() == 1) { - return new Person(id, name, surname); - } else { + if (statement.executeUpdate() != 1) { throw new IllegalArgumentException("name and surname can't be null"); } } @@ -110,36 +155,37 @@ public class PeopleDAO extends DAO { } } - public Person add(String name, String surname) + /** + * Removes a persisted person from the system. + * + * @param id identifier of the person to be deleted. + * @throws DAOException if an error happens while deleting the person. + * @throws IllegalArgumentException if the provided id does not corresponds + * with any persisted person. + */ + public void delete(int id) throws DAOException, IllegalArgumentException { - if (name == null || surname == null) { - throw new IllegalArgumentException("name and surname can't be null"); - } - - try (Connection conn = this.getConnection()) { - final String query = "INSERT INTO people VALUES(null, ?, ?)"; + try (final Connection conn = this.getConnection()) { + final String query = "DELETE FROM people WHERE id=?"; - try (PreparedStatement statement = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { - statement.setString(1, name); - statement.setString(2, surname); + try (final PreparedStatement statement = conn.prepareStatement(query)) { + statement.setInt(1, id); - if (statement.executeUpdate() == 1) { - try (ResultSet resultKeys = statement.getGeneratedKeys()) { - if (resultKeys.next()) { - return new Person(resultKeys.getInt(1), name, surname); - } else { - LOG.log(Level.SEVERE, "Error retrieving inserted id"); - throw new SQLException("Error retrieving inserted id"); - } - } - } else { - LOG.log(Level.SEVERE, "Error inserting value"); - throw new SQLException("Error inserting value"); + if (statement.executeUpdate() != 1) { + throw new IllegalArgumentException("Invalid id"); } } } catch (SQLException e) { - LOG.log(Level.SEVERE, "Error adding a person", e); + LOG.log(Level.SEVERE, "Error deleting a person", e); throw new DAOException(e); } } + + private Person rowToEntity(ResultSet row) throws SQLException { + return new Person( + row.getInt("id"), + row.getString("name"), + row.getString("surname") + ); + } } diff --git a/src/main/java/es/uvigo/esei/daa/dao/UsersDAO.java b/src/main/java/es/uvigo/esei/daa/dao/UsersDAO.java index 01460d9..61ea846 100644 --- a/src/main/java/es/uvigo/esei/daa/dao/UsersDAO.java +++ b/src/main/java/es/uvigo/esei/daa/dao/UsersDAO.java @@ -6,14 +6,33 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Base64; import java.util.logging.Level; import java.util.logging.Logger; +/** + * DAO class for managing the users of the system. + * + * @author Miguel Reboiro Jato + */ public class UsersDAO extends DAO { - private final static Logger LOG = Logger.getLogger("UsersDAO"); + private final static Logger LOG = Logger.getLogger(UsersDAO.class.getName()); - public String checkLogin(String login, String password) throws DAOException { + private final static String SALT = "daaexample-"; + + /** + * Checks if the provided credentials (login and password) correspond with a + * valid user registered in the system. + * + *

The password is stored in the system "salted" and encoded with the + * SHA-256 algorithm.

+ * + * @param login the login of the user. + * @param password the password of the user. + * @return {@code true} if the credentials are valid. {@code false} + * otherwise. + * @throws DAOException if an error happens while checking the credentials. + */ + public boolean checkLogin(String login, String password) throws DAOException { try (final Connection conn = this.getConnection()) { final String query = "SELECT password FROM users WHERE login=?"; @@ -23,15 +42,11 @@ public class UsersDAO extends DAO { try (final ResultSet result = statement.executeQuery()) { if (result.next()) { final String dbPassword = result.getString("password"); - final String shaPassword = encodeSha256(password); + final String shaPassword = encodeSha256(SALT + password); - if (shaPassword.equals(dbPassword)) { - return encodeBase64(login + ":" + password); - } else { - return null; - } + return shaPassword.equals(dbPassword); } else { - return null; + return false; } } } @@ -41,48 +56,6 @@ public class UsersDAO extends DAO { } } - public String checkToken(String token) - throws DAOException, IllegalArgumentException { - final String decodedToken = decodeBase64(token); - final int colonIndex = decodedToken.indexOf(':'); - - if (colonIndex < 0 || colonIndex == decodedToken.length()-1) { - throw new IllegalArgumentException("Invalid token"); - } - - final String login = decodedToken.substring(0, decodedToken.indexOf(':')); - final String password = encodeSha256(decodedToken.substring(decodedToken.indexOf(':') + 1)); - - try (final Connection conn = this.getConnection()) { - final String query = "SELECT password FROM users WHERE login=?"; - - try (final PreparedStatement statement = conn.prepareStatement(query)) { - statement.setString(1, login); - - try (final ResultSet result = statement.executeQuery()) { - if (result.next()) { - final String dbPassword = result.getString("password"); - - return password.equals(dbPassword) ? login : null; - } else { - return null; - } - } - } - } catch (SQLException e) { - LOG.log(Level.SEVERE, "Error checking token", e); - throw new DAOException(e); - } - } - - private final static String decodeBase64(String text) { - return new String(Base64.getDecoder().decode(text.getBytes())); - } - - private final static String encodeBase64(String text) { - return Base64.getEncoder().encodeToString(text.getBytes()); - } - private final static String encodeSha256(String text) { try { final MessageDigest digest = MessageDigest.getInstance("SHA-256"); diff --git a/src/main/java/es/uvigo/esei/daa/entities/Person.java b/src/main/java/es/uvigo/esei/daa/entities/Person.java index ae5c73f..3df16c4 100644 --- a/src/main/java/es/uvigo/esei/daa/entities/Person.java +++ b/src/main/java/es/uvigo/esei/daa/entities/Person.java @@ -1,41 +1,78 @@ package es.uvigo.esei.daa.entities; +import static java.util.Objects.requireNonNull; + +/** + * An entity that represents a person. + * + * @author Miguel Reboiro Jato + */ public class Person { private int id; private String name; private String surname; - public Person() { - } + // Constructor needed for the JSON conversion + Person() {} + /** + * Constructs a new instance of {@link Person}. + * + * @param id identifier of the person. + * @param name name of the person. + * @param surname surname of the person. + */ public Person(int id, String name, String surname) { this.id = id; - this.name = name; - this.surname = surname; + this.setName(name); + this.setSurname(surname); } + /** + * Returns the identifier of the person. + * + * @return the identifier of the person. + */ public int getId() { return id; } - public void setId(int id) { - this.id = id; - } - + /** + * Returns the name of the person. + * + * @return the name of the person. + */ public String getName() { return name; } + /** + * Set the name of this person. + * + * @param name the new name of the person. + * @throws NullPointerException if the {@code name} is {@code null}. + */ public void setName(String name) { - this.name = name; + this.name = requireNonNull(name, "Name can't be null"); } + /** + * Returns the surname of the person. + * + * @return the surname of the person. + */ public String getSurname() { return surname; } + /** + * Set the surname of this person. + * + * @param surname the new surname of the person. + * @throws NullPointerException if the {@code surname} is {@code null}. + */ public void setSurname(String surname) { - this.surname = surname; + this.surname = requireNonNull(surname, "Surname can't be null"); } @Override @@ -43,8 +80,6 @@ public class Person { final int prime = 31; int result = 1; result = prime * result + id; - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((surname == null) ? 0 : surname.hashCode()); return result; } @@ -59,16 +94,6 @@ public class Person { Person other = (Person) obj; if (id != other.id) return false; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (surname == null) { - if (other.surname != null) - return false; - } else if (!surname.equals(other.surname)) - return false; return true; } } diff --git a/src/main/java/es/uvigo/esei/daa/rest/PeopleResource.java b/src/main/java/es/uvigo/esei/daa/rest/PeopleResource.java index 9d69d30..09b8834 100644 --- a/src/main/java/es/uvigo/esei/daa/rest/PeopleResource.java +++ b/src/main/java/es/uvigo/esei/daa/rest/PeopleResource.java @@ -16,69 +16,130 @@ import javax.ws.rs.core.Response; import es.uvigo.esei.daa.dao.DAOException; import es.uvigo.esei.daa.dao.PeopleDAO; +import es.uvigo.esei.daa.entities.Person; +/** + * REST resource for managing people. + * + * @author Miguel Reboiro Jato. + */ @Path("/people") @Produces(MediaType.APPLICATION_JSON) public class PeopleResource { - private final static Logger LOG = Logger.getLogger("PeopleResource"); + private final static Logger LOG = Logger.getLogger(PeopleResource.class.getName()); private final PeopleDAO dao; + /** + * Constructs a new instance of {@link PeopleResource}. + */ public PeopleResource() { this(new PeopleDAO()); } - // For testing purposes + // Needed for testing purposes PeopleResource(PeopleDAO dao) { this.dao = dao; } - - @GET - public Response list() { - try { - return Response.ok(this.dao.list()).build(); - } catch (DAOException e) { - LOG.log(Level.SEVERE, "Error listing people", e); - return Response.serverError().entity(e.getMessage()).build(); - } - } + /** + * Returns a person with the provided identifier. + * + * @param id the identifier of the person to retrieve. + * @return a 200 OK response with a person that has the provided identifier. + * If the identifier does not corresponds with any user, a 400 Bad Request + * response with an error message will be returned. If an error happens + * while retrieving the list, a 500 Internal Server Error response with an + * error message will be returned. + */ @GET @Path("/{id}") public Response get( @PathParam("id") int id ) { try { - return Response.ok(this.dao.get(id), MediaType.APPLICATION_JSON).build(); + final Person person = this.dao.get(id); + + return Response.ok(person).build(); } catch (IllegalArgumentException iae) { LOG.log(Level.FINE, "Invalid person id in get method", iae); + return Response.status(Response.Status.BAD_REQUEST) - .entity(iae.getMessage()).build(); + .entity(iae.getMessage()) + .build(); } catch (DAOException e) { LOG.log(Level.SEVERE, "Error getting a person", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); + } + } + + /** + * Returns the complete list of people stored in the system. + * + * @return a 200 OK response with the complete list of people stored in the + * system. If an error happens while retrieving the list, a 500 Internal + * Server Error response with an error message will be returned. + */ + @GET + public Response list() { + try { + return Response.ok(this.dao.list()).build(); + } catch (DAOException e) { + LOG.log(Level.SEVERE, "Error listing people", e); return Response.serverError().entity(e.getMessage()).build(); } } - @DELETE - @Path("/{id}") - public Response delete( - @PathParam("id") int id + /** + * Creates a new person in the system. + * + * @param name the name of the new person. + * @param surname the surname of the new person. + * @return a 200 OK response with a person that has been created. If the + * name or the surname are not provided, a 400 Bad Request response with an + * error message will be returned. If an error happens while retrieving the + * list, a 500 Internal Server Error response with an error message will be + * returned. + */ + @POST + public Response add( + @FormParam("name") String name, + @FormParam("surname") String surname ) { try { - this.dao.delete(id); + final Person newPerson = this.dao.add(name, surname); - return Response.ok(id).build(); + return Response.ok(newPerson).build(); } catch (IllegalArgumentException iae) { - LOG.log(Level.FINE, "Invalid person id in delete method", iae); + LOG.log(Level.FINE, "Invalid person id in add method", iae); + return Response.status(Response.Status.BAD_REQUEST) - .entity(iae.getMessage()).build(); + .entity(iae.getMessage()) + .build(); } catch (DAOException e) { - LOG.log(Level.SEVERE, "Error deleting a person", e); - return Response.serverError().entity(e.getMessage()).build(); + LOG.log(Level.SEVERE, "Error adding a person", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); } } - + + /** + * Modifies the data of a person. + * + * @param id identifier of the person to modify. + * @param name the new name of the person. + * @param surname the new surname of the person. + * @return a 200 OK response with a person that has been modified. If the + * identifier does not corresponds with any user or the name or surname are + * not provided, a 400 Bad Request response with an error message will be + * returned. If an error happens while retrieving the list, a 500 Internal + * Server Error response with an error message will be returned. + */ @PUT @Path("/{id}") public Response modify( @@ -87,31 +148,64 @@ public class PeopleResource { @FormParam("surname") String surname ) { try { - return Response.ok(this.dao.modify(id, name, surname)).build(); + final Person modifiedPerson = new Person(id, name, surname); + this.dao.modify(modifiedPerson); + + return Response.ok(modifiedPerson).build(); + } catch (NullPointerException npe) { + final String message = String.format("Invalid data for person (name: %s, surname: %s)", name, surname); + + LOG.log(Level.FINE, message); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(message) + .build(); } catch (IllegalArgumentException iae) { LOG.log(Level.FINE, "Invalid person id in modify method", iae); + return Response.status(Response.Status.BAD_REQUEST) - .entity(iae.getMessage()).build(); + .entity(iae.getMessage()) + .build(); } catch (DAOException e) { LOG.log(Level.SEVERE, "Error modifying a person", e); - return Response.serverError().entity(e.getMessage()).build(); + + return Response.serverError() + .entity(e.getMessage()) + .build(); } } - - @POST - public Response add( - @FormParam("name") String name, - @FormParam("surname") String surname + + /** + * Deletes a person from the system. + * + * @param id the identifier of the person to be deleted. + * @return a 200 OK response with the identifier of the person that has + * been deleted. If the identifier does not corresponds with any user, a 400 + * Bad Request response with an error message will be returned. If an error + * happens while retrieving the list, a 500 Internal Server Error response + * with an error message will be returned. + */ + @DELETE + @Path("/{id}") + public Response delete( + @PathParam("id") int id ) { try { - return Response.ok(this.dao.add(name, surname)).build(); + this.dao.delete(id); + + return Response.ok(id).build(); } catch (IllegalArgumentException iae) { - LOG.log(Level.FINE, "Invalid person id in add method", iae); + LOG.log(Level.FINE, "Invalid person id in delete method", iae); + return Response.status(Response.Status.BAD_REQUEST) - .entity(iae.getMessage()).build(); + .entity(iae.getMessage()) + .build(); } catch (DAOException e) { - LOG.log(Level.SEVERE, "Error adding a person", e); - return Response.serverError().entity(e.getMessage()).build(); + LOG.log(Level.SEVERE, "Error deleting a person", e); + + return Response.serverError() + .entity(e.getMessage()) + .build(); } } } diff --git a/src/main/webapp/META-INF/context.xml b/src/main/webapp/META-INF/context.xml deleted file mode 100644 index 35bc2a7..0000000 --- a/src/main/webapp/META-INF/context.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index cc0a77f..f75cc40 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -1,23 +1,12 @@ - + DAAExample index.html - index.htm - index.jsp - default.html - default.htm - default.jsp - - - DAA Example DB Connection - jdbc/daaexample - javax.sql.DataSource - Container - \ No newline at end of file diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 87a10ae..a688425 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -6,9 +6,15 @@
-
Login:
-
Password:
-
+
+ Login: +
+
+ Password: +
+
+ +
\ No newline at end of file diff --git a/src/main/webapp/js/view/people.js b/src/main/webapp/js/view/people.js index 315d965..effe4e6 100644 --- a/src/main/webapp/js/view/people.js +++ b/src/main/webapp/js/view/people.js @@ -114,6 +114,8 @@ function appendToTable(person) { } function initPeople() { + // getScript permite importar otro script. En este caso, se importan las + // funciones de acceso a datos. $.getScript('js/dao/people.js', function() { listPeople(function(people) { $.each(people, function(key, person) { @@ -121,6 +123,8 @@ function initPeople() { }); }); + // La acción por defecto de enviar formulario (submit) se sobreescribe + // para que el envío sea a través de AJAX $(peopleFormQuery).submit(function(event) { var person = formToPerson(); diff --git a/src/main/webapp/main.html b/src/main/webapp/main.html index d8f1384..725e55f 100644 --- a/src/main/webapp/main.html +++ b/src/main/webapp/main.html @@ -10,7 +10,7 @@ Logout - +