viernes, 27 de abril de 2012

Postgres chkpass on hibernate (3)

In previous posts we've created a database with a table that includes a chkpass field and created the web service interface that will invoke the CRUD operations for this table. In this post we will connect our web service with the database using hibernate, therefore, we will have to map our chkpass field with its corresponding hibernate entity field.
First we will define the DTO that will be mapped to the postgres table, in order to accomplish that, we will:

  1. Create a pacage for DTOs: The package that will hold the DTOs in my project is com.dtorres.dto
  2. Create the DTO class: We will create a simple class: User that will handle the properties in the database, this is where the mapping to the chkpass will take action.

The User class will handle the User ID, User Name and Password fields to be created, read, updated and deleted from the database, therefore, it will have to handle getters and setters for those fields. Even though some of that fields are not for editing as the others:

  • User ID: In our database definition, this field is auto generated by a postgres sequence, therefore it will be not inserted, and as it is the Id of the record, it will be not updateable.
  • User Name: This field is handled as a unique index of the user record, they are intended to identify the user.
  • Password: This field is the one encrypted by the chkpass field type, it can be updated, but when retrieved, it is retrieved as encrypted password, so it has no direct update as the hibernate defines the field, we will create an special update method for it.

The code for now will be simple:

package com.dtorres.dto;

public class User {
  private long userId;
  private String userName;
  private String password;
  public long getUserId() {
    return userId;
  }
  public void setUserId(long userId) {
    this.userId = userId;
  }
  public String getUserName() {
    return userName;
  }
  public void setUserName(String userName) {
    this.userName = userName;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
}

Next, we will map this component using the persistence.xml file under the META-INF folder, we will change it's contents to something like this:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="pgIdentityManager">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>java:/identity-manager</jta-data-source>
    <class>com.dtorres.dto.User</class>
    <exclude-unlisted-classes>true</exclude-unlisted-classes>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
      <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
      <property name="hibernate.hbm2ddl.auto" value="validate" />
      <property name="hibernate.connection.autocommit" value="true" />
      <property name="show.sql" value="true" />
      <property name="hibernate.use_sql_comments" value="true" />
      <property name="hibernate.format_sql" value="true" />
    </properties>
  </persistence-unit>
</persistence>

Now our GUI might be complaining about the mapping of the com.dtorres.dto.User class, we will start annotating this class to stop this complaint. A simple mapping of the class and it's fields would be enough to have it mapped to a regular table with standard field types, but in this case, we are using a special field type, the chkpass field type, therefore, we must use the @TypeDef and @Type annotations to make hibernate know how this "special" field should be handled.

The UserType

UserType will be the superclass that we will use to make hibernate know the way that should be used to handle the chkpass. We will create a class that extends from this org.hibernate.usertype.UserType class. In my case, I'm creating the com.dtorres.customTypes.Chkpass class with this content:

package com.dtorres.customTypes;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;
import org.hsqldb.Types;
public class Chkpass implements UserType {
  @Override
  public Object nullSafeGet(ResultSet inResultSet, String[] names, Object o)
      throws HibernateException, SQLException {
    Object tmp = inResultSet.getObject(names[0]);
    return inResultSet.wasNull() ? null : tmp.toString();
  }
  @Override
  public void nullSafeSet(PreparedStatement inPreparedStatement, Object o,
      int i) throws HibernateException, SQLException {
    if (o == null)
      inPreparedStatement.setNull(i, Types.VARCHAR);
    else
      inPreparedStatement.setObject(i, o, Types.OTHER);
  }
  @Override
  public Object assemble(Serializable cached, Object owner)
      throws HibernateException {
    return cached;
  }
  @Override
  public Object deepCopy(Object o) throws HibernateException {
    if (o == null) {
      return null;
    }
    return new String(((String) o));
  }
  @Override
  public Serializable disassemble(Object value) throws HibernateException {
    return (Serializable) value;
  }
  @Override
  public boolean equals(Object x, Object y) throws HibernateException {
    return (x == y) || (x != null && y != null && (x.equals(y)));
  }
  @Override
  public int hashCode(Object x) throws HibernateException {
    return x.hashCode();
  }
  @Override
  public boolean isMutable() {
    return false;
  }
  @Override
  public Object replace(Object original, Object target, Object object)
      throws HibernateException {
    return original;
  }
  @Override
  public Class returnedClass() {
    return String.class;
  }
  @Override
  public int[] sqlTypes() {
    return new int[] { Types.OTHER };
  }
}

The important methods to note are the Object nullSafeGet(ResultSet inResultSet, String[] names, Object o) and the void nullSafeSet(PreparedStatement inPreparedStatement, Object o, int i). As you can see, the nullSafeGet method handles the result as a string result. In the nullSafeSet method we will use the inPreparedStatement.setObject(i, o, Types.OTHER); to specify that the input data must be handled as an object.
Once we have this type handler, we can continue with our mapping in the DTO.

Mapping the DTO

We will use the following annotation to map our new field type at the top of our class declaration: @TypeDefs({ @TypeDef(name="chkpass", typeClass=com.dtorres.customTypes.Chkpass.class) }). This @TypeDefs annotation will register our field type to be used by hibernate. Then, in our password column, we will use the @Type annotation to map the password column to the chkpass field type. the annotated entity class will be left as follows:

package com.dtorres.dto;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
@TypeDefs({ @TypeDef(name="chkpass", typeClass=com.dtorres.customTypes.Chkpass.class) })
@Entity
@Table(name="idm_user", uniqueConstraints = @UniqueConstraint(columnNames = { "user_name" }))
public class User {
  @Id
  @Column(name="user_id", insertable=false, updatable=false)
  @SequenceGenerator(name = "user_id_seq", sequenceName = "idm_user_user_id_seq")
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_id_seq")
  private long userId;
  @Column(name="user_name")
  private String userName;
  @Column(name="password", columnDefinition="chkpass", updatable=false, nullable=false)
  @Type(type = "chkpass")
  private String password;
  public long getUserId() {
  return userId;
  }
  public void setUserId(long userId) {
    this.userId = userId;
  }
  public String getUserName() {
    return userName;
  }
  public void setUserName(String userName) {
    this.userName = userName;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
}

Once our DTO is mapped with the field types, we will proceed to integrate our EntityManager in the Web Services and connect to the database.

Integrating with the Web Services

Figure 1: Current project structure in eclipse.

In our previous post we have created a session bean and annotated as web service in order to test the CRUD operations for our entity. In this section we will integrate this web service with the EntityManager and our just created entity DTO. At this point, if you have follow the steps from previous posts you should have a project like the one in the Figure 1. If you don't have the code to this point, please refer to comments, where I will publish the gitHub repo with the source code for this posts. Basically, the contents of the web service class are as follows:

package com.dtorres.ejb;

import javax.ejb.Stateless;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;

import com.dtorres.ejb.exceptions.PgIdentityException;

/**
 * Session Bean implementation class PgIdentityManager
 */
@Stateless(mappedName = "pgIdentityManager")
@WebService
public class PgIdentityManager implements PgIdentityManagerRemote {

@Override
@WebMethod
public String createUser(@WebParam(name="userName") String userName, @WebParam(name="password") String password)
throws PgIdentityException {
throw new PgIdentityException("Unimplemented method in service");
}

@Override
@WebMethod
public String getUser(@WebParam(name="userName") String userName) throws PgIdentityException {
throw new PgIdentityException("Unimplemented method in service");
}

@Override
@WebMethod
public String getAllUsers() throws PgIdentityException {
throw new PgIdentityException("Unimplemented method in service");
}

@Override
@WebMethod
public String deleteUser(@WebParam(name="userId") Long userId) throws PgIdentityException {
throw new PgIdentityException("Unimplemented method in service");
}

@Override
@WebMethod
public String resetPassword(@WebParam(name="userName") String userName, @WebParam(name="newPassword") String newPassword)
throws PgIdentityException {
throw new PgIdentityException("Unimplemented method in service");
}
}

We will change each method to implement the desired functionality for our project.
We will start by having an EntityManager. The EntityManager will handle the connections and requests for the database, we will not be worried about openning transactions or connections at this point, and when performance comes to the game we will be able to configure our EntityManager provider to fit with our requirements, as well as the transaction management. In order to include the EntityManager in our project we will add the following code to our web service class:

public class PgIdentityManager implements PgIdentityManagerRemote {
  @PersistenceContext
EntityManager entityManager;
...

Now we will preppare our first method, the createUser(String userName, String password) to validate the input and persist the required information into the database. In order to do that we will add the following code to our method:

@Override
@WebMethod
public String createUser(@WebParam(name="userName") String userName, @WebParam(name="password") String password)
    throws PgIdentityException {
    if (password.startsWith(":"))
      throw new PgIdentityException("Passwords are not allowed to begin with a colon symbol.");

    if (userName == null || "".equals(userName.trim()))
      throw new PgIdentityException("User Name is a required field");

    if (password == null || "".equals(password.trim()))
      throw new PgIdentityException("Password is a required field");

    if (userName.length() > 50)
      throw new PgIdentityException("User Name is too long [" + userName + "][" + userName.length() + "]");

    if (getUser(userName) != null)
      throw new PgIdentityException("The user [" + userName + "] already exists, try with another user name.");

    User user = new User();
    user.setUserName(userName);
    user.setPassword(password);

    entityManager.persist(user);
    return "Success";
}

I have to admit that the EntityManager will validate that the data fits the requirements of the database before commiting a transaction on it, my point for this validation before the entityManager.persist invokation is that I'd like to handle my exception description results, and if I leave the exception to the ejbContainer, I will lose some descriptive and pretty print exception description. If you have a finer way to accomplish this exception handling, please leave a comment in the blog, I will apreciate it. The real magic here is the entityManager.persist(user); instruction that will invoke the database using our database configuration, handle the transaction to it and commit the required INSERT for our record.
Also, we will implement the getUser(String userName) method by coding like this:

@Override
@WebMethod
public User getUser(@WebParam(name = "userName") String userName)
    throws PgIdentityException {
  String sQuery = "SELECT u FROM User u WHERE u.userName = '" + userName + "'";
  Query query = entityManager.createQuery(sQuery);
  return (User) query.getSingleResult();
}

The getAllUsers() method will be coded in a similar way:

@Override
@WebMethod
public List getAllUsers() throws PgIdentityException {
  String hquery = "select u from User u";
  Query query = entityManager.createQuery(hquery);
  return (List) query.getResultList();
}

Please note that when changing the result type of these methods, the compiler will complaint because the interface contract with the PgIdentityManagerRemote interface have been broken. Please change the return type and imports for the PgIdentityManagerRemote interface to fix the problem, our PgIdentityManagerRemote interface will be changed to:

package com.dtorres.ejb;

import java.util.List;
import javax.ejb.Remote;
import com.dtorres.dto.User;
import com.dtorres.ejb.exceptions.PgIdentityException;

@Remote
public interface PgIdentityManagerRemote {
  String createUser(String userName, String password) throws PgIdentityException;
  User getUser(String userName) throws PgIdentityException;
  List getAllUsers() throws PgIdentityException;
  String deleteUser(Long userId) throws PgIdentityException;
  String resetPassword(String userName, String newPassword) throws PgIdentityException;
}

The deleteUser(Long userId) implementation will implement its functionality in this way:

@Override
@WebMethod
public String deleteUser(@WebParam(name = "userId") Long userId)
    throws PgIdentityException {
  entityManager.remove(entityManager.merge(entityManager.find(User.class, userId)));
  return "Success";
}

Check the usage of the find method of the entityManager, which will use the @Id annotated property to create the SELECT statement.
Finally, our resetPassword method will be implemented with a more common UPDATE SQL statement that will handle the password change:

@Override
@WebMethod
public String resetPassword(@WebParam(name = "userName") String userName, @WebParam(name = "newPassword") String newPassword)
    throws PgIdentityException {

  if (newPassword.startsWith(":")) {
    throw new PgIdentityException("Password must not start with a colon.");
  }

  String updateNativeQueryString = "UPDATE idm_user SET password = '"
      + newPassword + "' WHERE user_name = '" + userName + "'";
  Query updateNativeQuery = entityManager.createNativeQuery(updateNativeQueryString);
  updateNativeQuery.executeUpdate();
  return "Success";
}

The final step to test that our code is working, will be to connect our server to the database and deploy the ejb Jar component on it.

Connect to the Database

I'm using Jboss server, and in this section, I will configure the datasource that will provide hibernate with the database connection. For that, we will create a pgIdentity-ds.xml file with the connection details and save that file in the $JBOSS_HOME/server/instance/deploy/ folder:

<?xml version="1.0" encoding="UTF-8"?>
<datasources xmlns:javaee="http://java.sun.com/xml/ns/javaee" xmlns:jboss="http://www.jboss.com/xml/ns/javaee" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.com/xml/ns/javaee">
  <local-tx-datasource>
    <jndi-name>identity-manager</jndi-name>
    <connection-url>jdbc:postgresql://localhost:5432/identity_manager</connection-url>
    <driver-class>org.postgresql.Driver</driver-class>
    <user-name>postgres</user-name>
    <password>secret</password>

    <prepared-statement-cache-size>10</prepared-statement-cache-size>
    <min-pool-size>10</min-pool-size>
    <max-pool-size>50</max-pool-size>
    <blocking-timeout-millis>5000</blocking-timeout-millis>
    <idle-timeout-minutes>15</idle-timeout-minutes>

  </local-tx-datasource>
  <!-- sql to call when connection is created. Can be anything, select 1 is valid for PostgreSQL -->
  <new-connection-sql>select 1</new-connection-sql>
  <!-- sql to call on an existing pooled connection when it is obtained from pool. Can be anything, select 1 is valid for PostgreSQL-->
  <check-valid-connection-sql>select 1</check-valid-connection-sql>
</datasources>

Once I publish my pgIdentity-ds.xml in the deploy folder, I will publish my ejb Jar in the same deploy folder, that will give me access to the new web services. I will use the SOAP-UI to test my web services, you can use any client you want. Have fun in changing the password and querying the database to retrieve the users with their encrypted password.

Feel free to ask any question, I will be happy to answer any concern that you might have.

1 comentario:

  1. Now you can see the source code of this exercise in github. Check this out: https://github.com/diego-torres/pgIdentityManager

    ResponderBorrar