diff --git a/.gitignore b/.gitignore index 4cf3d323a22e0f4f23901c630207be13c83ef862..4b3c842c179a5e0c3df54f268ee7aa8b5372da50 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 10438ca30cdd7f5bb2a50880ce7a58c208ea549d..23fcee57ae1efe8b623bd49dc002ef6fbc75744a 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 2210aa571f822aafa6db4715d625d45c64101a06..7c0a69cad0ea5f69219a5e9b8e0ec3bb467f4dd7 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 88e93d132d2a633ed21b39b0fdd265fc67bd9e0d..a7d1c3e8aa925ef999e0d6cc8e0f3cc46bfc0ad6 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 062bb86facfc4568a8f4c54747486bd6db016941..2105a513e20b8afac9b8b20a2939bb401262c2ec 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 7fbbbcb565ccb7eeb9c6449db4ff67c7e1555d52..9ee0e9e29bbc161ebae5beb1a5c25140425207c8 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 8e3cd50fa85cdf89a3cb6b321c5c94d4c00b2ecc..4860187882c93b75cc07fb1b7c935dd5573b2ef4 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 0c2320d17fc7c40039d51600b97407069d1601c6..1a83eee76744fcedcb13c5179da85cd2d1124a42 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 fda528bfa123ec4ad6c272dc0e7235b205a64438..ffd4233f07cfcbbed73c264c235efd0317b9ffcf 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 dfbbb13ca1c6bb28416d0d8eda6ffc0f87c85084..1d99edb26140ee773f06c8afbf0f40923f1848b2 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 01460d9af2de6eba3be423733c258f7e6049cf9d..61ea8464f0c0c1b31aebd36c9bb9143fa0b16642 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 ae5c73fb346a10935ea1ea04b0a6e0e3730f0054..3df16c413d162b38bc5092b2f687e5255a2210bf 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 9d69d300c463f766a4f8f95a5c611a77fd852360..09b8834c7b71d5176e012bcd6076d36eb275bb29 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 35bc2a76c7895ab4805dd6adb71fa848af3409a4..0000000000000000000000000000000000000000 --- 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 cc0a77fd28856b853b76cf6d1082f27e4583e6fa..f75cc40fb869362880bd9d5d66a911e829ce6790 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 87a10ae81d9dafc2c6d4dfbfd59115c261ffd76a..a688425bd7cdd100847d70fdf17e0ec12d227263 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 315d965a6af8bcb0fcf24778c1c6fb19df168204..effe4e6a898c1aef198b78fa7517987e746304c5 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 d8f13844f896e89dae6c647e46323bf952aa9bb1..725e55f04f89991163751dd63e8ee1bc9b0367f9 100644 --- a/src/main/webapp/main.html +++ b/src/main/webapp/main.html @@ -10,7 +10,7 @@ Logout - +