PARTE 1: PREPARACIÓN DEL IDE (ECLIPSE)
AJAX
AJAX (Asynchronous javascript And XML), es una tecnología que es muy usada en el desarrollo de sitios o aplicaciones web. Ésta tecnología nos permite mostrar, recargar contenido sin necesidad de refrescar toda la página web.
Esto es muy útil, sobre todo desde el punto de vista de rendimiento ya que, de la forma normal cuando se recarga toda la página, se vuelven a realizar todas las peticiones que ya han sido mandadas, como son peticiones a tu hoja de estilos, a tus scripts JS, fuentes, librerías. Por otro lado, con AJAX, solo se recarga la parte que se desea.
HIBERNATE
Hibernate es un framework ORM (Object Relational Mapping). Un ORM es una aplicación que nos permite “mapear” nuestras tablas y mostrarlas en nuestra aplicación como “objetos” (Si no sabes qué es un objeto lee primero sobre POO y vuelve cuando ya hayas asimilado los conceptos).
Cuando trabajamos con un ORM, nosotros ya no tenemos que preocuparnos por iniciar la conexión, cerrarla ni construir sentencias SQL. El ORM lo hace por nosotros. Alternativamente, Hibernate nos permite utilizar “NamedQueries” que son sentencias HQL (Hibernate Query Language) y poder llamarlas cuando las necesitemos.
LEVANTAMIENTO DEL PROYECTO
Herramientas:
- Eclipse Luna JEE
- MySQL 5.6
- Hibernate 4.3.8
- Java-JSON
- jBCrypt
Creamos una tabla en MySQL llamada “users4login” en nuestra BBDD. Ésta tabla tendrá los siguientes atributos (vamos a mantenerlo simple):Código- CREATE TABLE users4login
- (
- `user_id` INT AUTO_INCREMENT NOT NULL,
- `username` VARCHAR(45) NOT NULL,
- `email` VARCHAR(90) NOT NULL,
- `password` VARCHAR(255) NOT NULL,
- CONSTRAINT pk_user PRIMARY KEY(`user_id`)
- );
A continuación ingresamos los siguientes datos:Código:username: Duke
email: duke@localtest.me
password: 12345 (hasheado en BCrypt):
$2a$10$1Yf2nGEJWacanDyfftzpru8vcy4L5bOB6ohJ9bG4FeSg1T568DGWaPREPARACIÓN DEL ENTORNO
Registrar el driver MySQL en Eclipse:
Nos dirigimos a Eclipse y clickeamos en:
Código:Window -> Preferences.
Se abrirá una ventana con muchas opciones. Nos dirigimos a:Código:Data Management -> Conectivity -> Driver definitions
Seleccionamos Add y se nos abrirá la siguiente ventana:
Desplegamos el combo “Vendor Filter” y seleccionamos “MySQL”. Escojemos la versión 5.1. Ahora, nos dirigmos a la pestaña “JAR list”.
Aquí hacemos click en “Add JAR/Zip” y nos mostrará un diálogo para seleccionar el conector jdbc para MySQL. Navegamos hacia donde tengamos el conector y lo seleccionamos. Una vez seleccionado el conector, hacemos clic en “OK”.
NOTA: Si no tenemos el conector, lo podemos bajar de Aquí.CREACIÓN DE UN DATA SOURCE
Nos dirigimos hacia “data source explorer” en Eclipse y en “Database Connections” hacemos click derecho y seleccionamos “New..”.
En la ventana que nos mostrará, podemos ver todos los gestores de bases de datos. Seleccionamos MySQL y le colocamos el nombre “Users_Test”. Damos click en “Next”. Por último, colocamos los datos de nuestra BBDD y le damos finish:REGISTRAR UN SERVIDOR DE APLICACIONES (TOMCAT)
Nos dirigimos a:
Código:Window -> Preferences -> Server -> Runtime enviroments
En la siguiente pantalla, solo indicamos la ruta donde tenemos descomprimido Tomcat 8 y le damos finish.
NOTA: Si no lo tienes, lo puedes descargar desde Aquí.
Aceptamos todo y nos dirigimos en la pestaña Servers del panel derecho. Aquí damos click derecho en el panel y seleccionamos:Código:New -> Server
Y finalmente, seleccionamos nuestro servidor Tomcat 8 que hemos registrado y le damos finish:
Ya tenemos listo nuestro IDE para poder trabajar.PARTE 2: CREACIÓN Y EJECUCIÓN DEL PROYECTO
Nos dirigimos a:Código:New -> Project -> JPA Project
Le colocamos el nombre “LoginDemo”. Escojemos nuestro servidor Tomcat 8, hacemos click en “Modify” y seleccionamos lo siguiente:
Aceptamos y damos click en Next. Aquí nos pedirá la conexión a la BBDD para poder mapear las tablas a clases. Seleccionamos nuestra conexión y Next:
Debe quedar así. Por último, en la siguiente pantalla seleccionamos la casilla “Generate web xml deployment descriptor” y le damos finish.
Nuestro proyecto debe verse así:
Dentro de WEB-INF hay una carpeta lib. Ésta carpeta contiene las librerías que necesita la aplicación, por lo tanto, añadimos las librerías de Hibernate, MySQL, JSON y BCrypt (éstas dos últimas las veremos más adelante). Debe quedar así:
Al estar en esta carpeta, Eclipse automáticamente las agrega al classpath.
Ahora, necesitamos especificar la configuración de nuestra conexión a la BBDD. El archivo persistence.xml queda así:Código- <?xml version="1.0" encoding="UTF-8"?>
- <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
- <!-- nombre de nuestra persistencia y tipo de transacción (local) -->
- <persistence-unit name="LoginDemo" transaction-type="RESOURCE_LOCAL">
- <!-- proveedor jpa - hibernate -->
- <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
- <!-- configuracion -->
- <properties>
- <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/test_db"/>
- <property name="javax.persistence.jdbc.user" value="usuario"/>
- <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
- <property name="javax.persistence.jdbc.password" value="contra"/>
- <property name="dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
- <property name="hibernate.hbm2ddl.auto" value="update"/>
- <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
- <property name="show_sql" value="true"/>
- </properties>
- </persistence-unit>
- </persistence>
- Primero le damos un nombre a la unidad de persistencia, en éste caso LoginDemo.
- Indicamos el proveedor de la persistencia. En éste caso Hibernate.
- Indicamos la configuración de la conexión.
Hay que prestar especial atención en el atributo name de persistence-unit. El nombre que le pongas, será el que debes especificar más adelante cuando inicialices Hibernate.
NOTA: Las siguientes líneas dependen de tu usuario en MySQL y tu contraseña:
Código
<property name="javax.persistence.jdbc.user" value="usuario"/> <property name="javax.persistence.jdbc.password" value="contra"/>
Una vez hecho esto, procedemos a mapear nuestra tabla users4login a una clase.
MAPEO DE TABLAS
Primero creamos un paquete:
Código:
Click derecho en src -> New-> Package
Le colocamos el nombre: com.mycompany.models.entities
Luego, mapeamos nuestra tabla a una clase:
Código:
Click derecho en com.mycompany.model.entities -> New-> JPA entities from tables
Veremos la siguiente ventana. Aquí seleccionamos nuestra conexión y las tablas a mapear:
Finalmente damos click en Finish. Y se nos creará una clase. Vamos a modifcarla un poco, y quedará así:
Código
package com.mycompany.models.entities; import java.io.Serializable; import javax.persistence.*; /** * The persistent class for the users4login database table. * */ @Table(name="users4login") @NamedQueries({ @NamedQuery(name="Users4login.findAll", query="SELECT u FROM User u"), @NamedQuery(name="Users4login.findUser", query = "SELECT u FROM User u WHERE u.username = :username OR u.email = :username") }) private static final long serialVersionUID = 1L; @Id @Column(name="user_id",nullable=false) @GeneratedValue(strategy = GenerationType.IDENTITY) private int userId; @Column(name="email",nullable=false) @Column(name="password",nullable=false) @Column(name="username",nullable=false) public User() { } public int getUserId() { return this.userId; } public void setUserId(int userId) { this.userId = userId; } return this.email; } this.email = email; } return this.password; } this.password = password; } return this.username; } this.username = username; } }
Aquí voy a explicar algunas cosas:
- @Table: hace referencia a la tabla en la BBDD y el atributo name indica el nombre de la tabla. Por ésta, razón le podemos poner el nombre que queramos a la clase. Para hacerlo más sencillo, se lo he cambiado por “User” en lugar de “Users4login”.
- @Entity: indica que dicha clase es una entidad.
- @Id: Indica que dicho campo es la llave primaria.
- @GeneratedValue: indica el tipo de generación, en éste caso GenerationType.IDENTITY, indica que la llave primaria tendrá un generador AUTO_INCREMENT en MySQL.
- @Column: indica que dicho campo es una columna en la base de datos. El atributo name, indica el nombre de la columna y nullable, si el campo puede recibir null.
- @NamedQuery: especifica una consulta a la base de datos y se identifica con un nombre (atributo name).
Una vez explicado éstos conceptos, creamos un paquete llamado com.mycompany.util. Dentro de éste paquete, creamos una clase llamada EntityManagerUtil:
Código
package com.mycompany.util; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; /** * * @author Gus */ public class EntityManagerUtil { private static final EntityManagerFactory emf = Persistence.createEntityManagerFactory("LoginDemo"); private EntityManagerUtil() { } public static EntityManagerFactory getEntityManagerFactory() { return emf; } }
Lo que hace ésta clase es leer el archivo persistence.xml y a partir de él, crear una factoría de EntityManager. EntityManager, como su nombre lo indica, es el encargado de administrar entidades (leer, persistir, actualizar, eliminar).
Aquí es cuando el atributo name del persistence-unit del archivo persistence.xml tiene importancia. "LoginDemo", es el nombre que le hemos puesto al persistence-unit, por lo tanto, debemos indicárselo en el método createEntityManagerFactory().
En ese mismo paquete, creamos una nueva clase llamada UserUtil, la cual hará las consultas a la BBDD para comprobar si un usuario existe en la tabla.
Código
package com.mycompany.util; import java.util.List; import javax.persistence.*; import com.mycompany.models.entities.User; /** * * @author Gus */ public class UserUtil { { EntityManager em = null; User user = null; /** * @description Obtenemos el EMF y a continuación el EM. Luego iniciamos una * transacción (em.getTransaction().begin()). Posteriormente cargamos la NamedQuery * que creamos en la clase 'User', y le asignamos el parámetro ':username' (es equivalente * al '?' de las sentencias preparadas), en este caso el nombre de usuario/email que recibe * por parámetro. Ejecuta la sentencia y guarda - si encuentra - el usuario en la variable 'user'. * Finalmente termina la transacción. * @error Pueden ocurrir dos errores: Un error al obtener el EM o al no encontrarse el usuario. * @after Cerramos la conexión del EM y devolvemos la varianle 'user'. */ try { em = EntityManagerUtil.getEntityManagerFactory().createEntityManager(); em.getTransaction().begin(); Query query = em.createNamedQuery("User.findUser",User.class); query.setParameter("username", username); @SuppressWarnings("unchecked") List<User> users = (List<User>) query.getResultList(); if(!users.isEmpty()) user = (User) users.get(0); em.getTransaction().commit(); } { ex.printStackTrace(); if(em.getTransaction().isActive()) em.getTransaction().rollback(); } } finally { if(em != null && em.getTransaction().isActive()) { em.close(); } } return user; } }
Ahora bien, ¿Cómo trabaja JPA y qué significa éste código? A continuación, paso a explicarlo:
Éste método recibe como parámetro un nombre de usuario o email. A continuación, creamos un objeto tipo EntityManager (el cual ya expliqué más arriba su propósito) y un objeto tipo User para devolverlo en caso hayan coincidencias.
Dentro del try, hago una llamada al método estático getEntityManagerFactory() de la clase EntityManagerUtil, el cual me devuelve una instancia tipo EntityManagerFactory, seguidamente contateno ésta llamada al método propio de EntityManagerFactor, getEntityManager(), el cual me devolverá una instancia de tipo EntityManager para poder manejar entidades (entidad User en nuestro caso).
Obtengo una instancia Transaction del objeto EntityManager (em.getTransaction()) e inicio una nueva transacción llamando al método begin de Transaction (em.getTransaction().begin()). Listo, tengo mi transacción lista para realizar operaciones.
Creo una NamedQuery con la NamedQuery que creamos en la entidad User. El método createNamedQuery recibe dos parámetros: El nombre de la NamedQuery y la entidad propietaria. Por ésta razón, seguido del nombre de la NamedQuery le pasamos la clase User (User.class). Éste método devuelve un objeto Query construido con los parámetros enviados listo para realizar la consulta.
Antes de realizar una consulta con un Query, necesitamos establecer los valores que hemos dejado en “duda” en nuestra NamedQuery de la entidad User:
Código
@NamedQuery(name="Users4login.findUser", query = "SELECT u FROM User u WHERE u.username = :username OR u.email = :username")
Lo que es equivalente a un PreparedStatement o a un stmt en PHP:
Código
ps.setString(1,”usuario”); ps.setString(2,”email@algo.com”);
PHP:
Código
Stmt.Bind_param(“ss”,”username”,”email@algo.com”);
Tomar atención que la NamedQuery no hace referencia al nombre de la tabla, si no al nombre de la clase, porque la clase hace referencia a la tabla.
- Ejecutamos la sentencia: query.getResultList(). Éste método devuelve una lista de registros de acuerdo a la consulta.
- Evaluamos, si la lista no está vacía guardamos en el objeto User que creamos al inicio del método, el Usuario encontrado traído de la BBDD.
- Por último, cerramos la transacción: em.getTransaction().commit() y retornamos el objeto User declarado al principio del método, independientemente si está null (no se encontró Usuario).
- En el catch, si la la conexión del EntityManager está activa, hacemos un rollback(), que es deshacer cambios.
- En el finally, cerramos la conexión del EntityManager, sólo si ésta está activa.
En el mismo paquete, crearemos una clase llamada JSONUtil, con el siguiente código:
Código
package com.mycompany.util; import java.util.HashMap; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; public class JSONUtil { { JSONObject jsonObj = null; Map<String,Object> data = null; try { jsonObj = new JSONObject(stringifyJSON); data = new HashMap<>(); for(byte i=0; i<jsonObj.names().length(); i++) { data.put(jsonObj.names().getString(i), jsonObj.get(jsonObj.names().getString(i))); } } catch (JSONException e) { e.printStackTrace(); } return data; } }
Ésta clase tiene un método bien simple. Recibe un objeto JSON en tipo String, lo convierte a JSON con ayuda de la librería json-java, lo recorre y guarda los datos que tenga en un objeto Map (si vienes de PHP, imagina que son arrays asociativos).
JSONObject.names() devuelve un objeto JSON con las llaves del objeto JSON. Y JSONObject.get(String key), devuelve el valor asociado a dicha llave.
La estructura del proyecto hasta el momento está así:
Antes de proceder:
- Crear un folder dentro de WebContent llamado resources.
- Dentro de resources crear 3 folders: css, js, img y vendor.
- Descargar jQuery 2.1.1 en su versión minificada y colocarla dentro del folder vendor.
- Descargar la hoja de estilos por ser un poco larga, y colocarla dentro del folder css.
Así quedaría nuestra estructura:
Creamos un archivo llamado index.jsp dentro del folder WebContent:
Código:
New -> JSP file.
Éste archivo tendrá el siguiente código:
Código
<%-- Document : index Created on : 21/01/2015, 09:51:38 AM Author : Gus --%> <%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="resources/css/font-awesome.min.css"/> <link rel="stylesheet" href="resources/css/general.css"/> </head> <body> <section class="main-container"> <header> <nav class="navbar"> </nav> </header> <section class="main-content"> <form class="panel"> <section class="panel-head"> </section> <section class="panel-body"> <section class="form-group-hoz"> <input type="text" id="user" name="user" class="textfield"/> </section> <section class="form-group-hoz"> <input type="password" id="pass" name="pass" class="textfield"/> </section> <section id="error-message" class="error-message-inactive"> </section> <section class="form-btn-group"> </section> </section> </form> </section> </section> </body> </html>
Lo que nos interesa es solamente el formulario. El resto, es solo para darle estilos a la página y se vea formal. En el formulario, no especificamos el método ni el archivo que procesará la acción. Esto lo haremos posteriormente con AJAX.
Podemos ver 2 elementos tipo input, uno para el usuario y otro para la contraseña y le colocamos un id cada uno para poder identificarlos:
Código
También contiene dos botones: Uno para enviar los datos del formulario y otro para limpiar.
Código
CREACIÓN DEL ARCHIVO javascript (AJAX)
Creamos un archivo javascript llamado call_servlet_ajax.js dentro del folder resources/js. Éste archivo tendrá el siguiente código.
Código
/** * @author Gus */ $("form").on("submit", function(e) { // previene el submit normal del formulario e.preventDefault(); var username = $("#user").val(); var pass = $("#pass").val(); var dataToSend = '{"username": "'+username+'", "password": "'+pass+'"}'; $.ajax( { url: "LoginController", data: { data: dataToSend }, dataType: "json", type: "post" }) .done(function(data) { console.log("SUCCESS"); $("#error-message > p").html(data["message"]); $("#error-message > p").removeClass("error-message-text").addClass("success-message-text"); $("#error-message > p").css("padding",".25rem"); if($("#error-message").hasClass("error-message-inactive")) { $("#error-message").removeClass("error-message-inactive").addClass("error-message-active"); } }) .fail(function(jqXHR, textStatus, errorThrown) { var response = JSON.parse(jqXHR.responseText); console.log("FAIL"); $("#error-message > p").html(response["message"]); $("#error-message > p").removeClass("success-message-text").addClass("error-message-text"); $("#error-message > p").css("padding",".25rem"); $("#error-message").removeClass("error-message-inactive").addClass("error-message-active"); }) .always(function(jqXHR, textStatus, errorThrown) { $("#loading-icon").removeClass("fa fa-circle-o-notch fa-spin"); }); });
Primero obtenemos los valores escritos en los input. Seguidamente creamos un objeto JSON con dichos valores.
En la llamada AJAX, en la url deben poner el nombre del servlet (sin la barra al inicio), para indicar que dicho servlet manejará la petición. En data, creamos un JSON con un parámetro “data” y su valor el String con forma de JSON “dataToSend”, especificamos que se va a trabajar con json y que el método será POST.
CREACIÓN DEL SERVLET
Creamos un paquete llamado com.mycompany.controllers.servlets. Dentro de éste paquete crearemos un Servlet:
Código:
New -> Servlet
Y lo pondremos el nombre LoginController. Damos finish. Colocamos el siguiente código en el Servlet:
Código
package com.mycompany.control.servlets; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.json.JSONObject; import org.mindrot.jbcrypt.BCrypt; import com.mycompany.models.entities.User; import com.mycompany.util.JSONUtil; import com.mycompany.util.UserUtil; @WebServlet(asyncSupported = true, urlPatterns = { "/LoginController" }) public class LoginController extends HttpServlet { private static final long serialVersionUID = 1L; public LoginController() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("application/json"); Map<String,Object> data = JSONUtil.jsonToMap(request.getParameter("data")); Map<String,Object> operationInfo; // contiene mensajes de los sucesos de la operación if(user != null) { operationInfo = new HashMap<>(); // si el password ingresado coincide con el password del usuario hasheado -> Login correcto { operationInfo.put("message", "Login correcto"); } else { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); operationInfo.put("message", "Contreña incorrecta"); } writer.print(new JSONObject(operationInfo)); } else { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); operationInfo = new HashMap<>(); operationInfo.put("message", "Usuario no encontrado"); writer.print(new JSONObject(operationInfo)); } writer.flush(); } }
Lo primero que hacemos es establecer una cabecera para el tipo de respuesta. Como vamos a devolver un archivo JSON, establecemos el content type como “application/json”. Luego obtenemos el objeto PrintWriter para mostrar datos en pantalla.
Aquí viene algo interesante. Hacemos una llamada al método estático jsonToMap de la clase JSONUtil que hemos creado y le pasamos lo siguiente:
Código
¿Qué significa esto? Bien, si recuerdas, en el archivo call_servlet_ajax, en la data a enviar enviamos un JSON que tenía la llave “data” y el valor “dataToSend” que era un String con forma de JSON.
Bien, request.getAtributte(“data”), obtiene el valor de la llave “data” que le hemos enviado mediante AJAX. Entonces, ¿qué hace el método jsonToMap con éste JSON en formato String que hemos obtenido?
Lo convierte a JSON con:
Código
E itera el JSON para guardar las llaves y los valores en un Map (array asociativo) y lo devuelve. Entonces, el objeto Map data del Servlet, recibe el objeto Map que retorna el método jsonToMap.
jsonObj = new JSONObject(stringifyJSON);
Crea un objeto Map para guardar los mensajes dependiendo de los sucesos y enviarlos de vuelta por AJAX. Crea un objeto User y utiliza el método estático getUser4login de UserUtil, pasándole el nombre de usuario para que el método consulte si existe y devuelva el Usuario encontrado o null si no lo encuetra.
Evaluamos, si el usuario ha sido encontrado, comparamos las contraseñas. Para esto, hacemos uso del método estático checkpw de la librería jBCrypt para verificar la contraseña enviada por el formulario con la contraseña que está hasheada en la BBDD.
Si la contraseña coincide, guardamos en el mapa el mensaje “Login correcto”. Si la contraseña no coincide, guardamos en el mapa el mensaje “Contraseña incorrecta”.
Finalmente devolvemos el mapa convertido en JSON pasándole el mapa al constructor de la clase JSONObject. Una vez devuelto el objeto JSON al archivo javascript, podremos mostrar los mensajes que le hemos enviado desde el Servlet.
En el método success, simplemente se le asigna al elemento p hijo de #error-message el texto enviado desde el Servlet. Además de un cambio de clases CSS para mostrar el mensaje con un efecto de “fade”, en el método fail, exactamente lo mismo.
RESULTADO FINAL
Logueo exitoso:
Logueo con error de validación: