Monday, April 2, 2007

Hibernate CompositeUserType and X509Certificate

Often, when working with Hibernate as your ORM tool, you find the need to persist an object to the database that doesn't match one of the supported types. For example, what if you wanted to persist an XML object? One such way would be to convert the XML to a String type and persist it that way. Of course, that messes up our object model, doesn't it? Wouldn't it be nice if a setXXX() method could handle the XML object directly? Using Hibernate's UserType object, we have the flexibility to handle these types of conversions outside of our object model.

I was faced with a very similar problem while reworking the way X.509 certificates are persisted in OdyssiCS. Previously, I would conver the certificate to a byte[] and store its base64 encoded representation in the database. This resulted in a very ugly object model. I contemplated making use of a UserType object for my X.509 certificate, but was then faced with a secondary problem: How would I be able to perform queries against properties of the certificate? What if I wanted to locate a certificate with a specific serial number? One approach would be to maintain those properties separately. However, this results, again, in a polluted object model. After further review, I decided upon Hibernate's CompositeUserType object. A CompositeUserType is an extension of UserType that allows for properties to be derefferenced when performing a Hibernate query. Since I couldn't find much information about how to work with CompositeUserType objects, I decided to post some of my findings here to serve as a reference to others. First, let's look at what we want our end result to be.

In my object model, I have a CertificateModelImpl class that contains one method related to what we're doing with CompositeUserType: getCertificate(). This method returns the X.509 certificate object. Our database has a table called certificate_tbl with the following columns:

  • ID -- A simple incremented row identifier
  • SUBJECT_ID -- Corresponds to the table containing our subject distinguished names
  • START_DATE -- The start of the certificate validity
  • END_DATE -- The expiration date for the certificate
  • SERIAL_NUMBER -- The certificate serial number
  • CERTIFICATE_DATA -- Contains the certificate in byte[] format
In our relational model, SUBJECT_ID is a foreign key pointing to another table. For our discussion, it is not relavant. We would like to focus on the remaining columns. Our end goal is this: We want to be able to call setCertificate() on CertificateModel, passing an X509Certificate object as a parameter. We also need to be able to use properties of the certificate, such as serial number and expiration date, in a Hibernate query. We want to store the certificate properties in the corresponding columns of the table, but don't want them available to our object model. If these properties were available, we could run into an issue with data inconsistancy.

To start implementing our X509CertificateUserType object, we must implement a couple of preliminary methods. The returnedClass() method tells Hibernate what type of object we are dealing with. In our case, we return an X509Certificate.class object. The equals() and hashCode() methods are pretty standard fare, acting as they normally do in Java. There are a couple of other methods that must be implemented, such as deepCopy() and isMutable(). Take a look at the Hibernate Javadocs for more information on what the remaining methods accomplish.

Our first goal is to be able to store an X509Certificate object in the database as a byte[] and retrieve it later as an X509Certificate object. The two methods of CompositeUserType that accomplish this are nullSafeSet() and nullSafeGet(). The nullSafeSet() method is responsible for converting the certificate to a byte[] and storing it in the appropriate column. This is also where we store the other certificate properties, such as expiration information and serial number. For our puirposes, this is what the implementation of this method looks like:


public void nullSafeSet(PreparedStatement preparedStatement, Object object,
int i, SessionImplementor sessionImplementor) throws HibernateException, SQLException {
X509Certificate cert = (X509Certificate) object;
Hibernate.DATE.nullSafeSet(preparedStatement, cert.getNotBefore(), i);
Hibernate.DATE.nullSafeSet(preparedStatement, cert.getNotAfter(), i + 1);
Hibernate.BIG_INTEGER.nullSafeSet(preparedStatement, cert.getSerialNumber(), i + 2);

try {
preparedStatement.setBytes(i + 3, cert.getEncoded());
} catch (CertificateEncodingException e) {
throw new HibernateException(e);
}
}


You see here that we, first, set the additional properties for certificate. How do we know which columns the values are set in? This is set as part of our Hibernate mapping file, which I will explain later. From there, we convert the certificate to a byte[] and set that in the database as well. We're now ready to implement the method for retrieving the certificate from the database. Our nullSafeGet() implementation looks like this:



public Object nullSafeGet(ResultSet resultSet, String[] strings,
SessionImplementor sessionImplementor, Object object)
throws HibernateException, SQLException {
X509Certificate cert = null;
byte[] certData = resultSet.getBytes(strings[3]);
if (!resultSet.wasNull()) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certData));
} catch (CertificateException e) {
throw new HibernateException(e);
}
}
return cert;
}


Here, you'll se we create a CertificateFactory object to parse our byte[] into an X509Certificate object. At no point do we concern ourselves with the additional certificate properties we set earlier. Our only concern is the certificate itself. At this point, we can store and retrieve an X509Certificate object with no problems.

We now need the ability to reference properties of an X509Certificate in a Hibernate query to simplify with searching. The getPropertyNames(), getPropertyTypes(), getPropertyValue(), and setPropertyValue() methods of CompositeUserType assist with this goal. The getPropertyNames() method returns the names of the properties we wish to make available when querying for a certificate. In our case, these values are:

  • issueDate
  • expirationDate
  • serialNumber
  • certificateData
The getPropertyTypes() method specifies the Hibernate type that corresponds to each of these properties. So, in our case, the following types would be returned:
  • Hibernate.DATE
  • Hibernate.DATE
  • Hibernate.BIG_INTEGER
  • Hibernate.BLOB
The getPropertyValue() method is responsible for determining which property is being queried and then returning the appropriate value. In our case, the implementation looks like this:


public Object getPropertyValue(Object object, int i) throws HibernateException {
if (object == null) {
return null;
}
X509Certificate cert = (X509Certificate) object;
if (i == 0) {
return cert.getNotBefore();
} else if (i == 1) {
return cert.getNotAfter();
} else if (i == 2) {
return cert.getSerialNumber();
} else if (i == 3) {
try {
return cert.getEncoded();
} catch (CertificateEncodingException e) {
throw new HibernateException(e);
}
} else {
return null;
}
}


The final method, setPropertyValue(), does nothing in our implementation. We, obviously, don't want the certificate properties to be modifiable, so our method implementation simply does nothing and then returns.

Now that we have implemented the necessary methods, we need to create the Hibernate mapping for our object. In our CertificateModelImpl.hbm.xml file, we have the following mapping for our certificate property:


<property name="certificate" type="net.odyssi.certserv.dao.hibernate.model.X509CertificateUserType">
<column null="true" type="date" name="start_date">
<column null="true" type="time" name="expiration_date">
<column null="true" type="integer" name="serial_number">
<column null="true" type="blob" name="certificate_data">
</property>


That's all there is to it! Hibernate can now store and retrieve X509Certificate objects with no problem, and queries can reference properties of that certificate. To see the finished product, take a look at X509CertificateUserType in the OdyssiCS CVS repository.