Using Trails with JBoss and the Hibernate MBean
by Gert Jan Verhoog
Func. Internet Integration
| Not Up To Date 2005-12-30 Solved latest issues - story will be updated next week. For now: don't try this at home yet |
Introduction
This article sketches a possible way in which Trails can be deployed in JBoss using a shared Hibernate session factory. Both the implementation and this article may have some rough edges. Please regard this as a proof of concept, not necessarily production quality code.
The problem
Trails uses Hibernate as its persistence framework; see a VeryShortOverviewOfTrails for more info. One of Trails' strong proints is that it automatically detects which of your java beans are mapped with Hibernate to persistent storage. Trails gets the necessary information from a Hibernate Configuration object, retrieved from the sessionFactory bean, configured like this:
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> . . . </bean>
But what if the sessionFactory bean is of a different type? When using JBoss and a shared Hibernate session factory, the sessionFactory bean is configured like this:
<bean id="sessionFactory" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"><value>java:/hibernate/SessionFactory</value></property> </bean>
The type of this bean is more generic than the LocalSessionFactoryBean defined above. Specifically, the method getConfiguration() is not available. This method is used by the HibernateDescriptorDecorator to build descriptors for mapped beans. This short article tries to provide a solution to this problem.
Using Trails with JBoss
We use JBoss 4.0.3RC1 with a shared Hibernate session factory and datasource. The datasource and session factory are available through jndi. For more information, consult the JBoss documentation.
In the following sections, 'JBOSS' is used in paths to denote the directory where JBoss is installed. JBoss' default server configuration is used.
Jars and class loader issues
In JBOSS/server/default/lib, the following jars should be present:
- your jdbc driver (e.g. jtds, postgresql or another driver)
- all hibernate jars
Important: class loading troubles
It is very important that the Hibernate jars are NOT present in war archives that use the shared session factory. This would result in your war not seeing the shared session factory.
Another issue is with trails, more specifically Hivemind (used by Tapestry, used by Trails): if trails.jar is present in both the lib directory and your war, Hivemind throws an exception because some services are defined multiple times. If trails is only present in the lib directory, your Trails application doesn't see the trails classes and bails. After some trial and error, the only combination that seemed to work for me was: trails.jar not in lib, only in the war archive.
The data source
in JBOSS/server/default/deploy, place a file my-ds.xml (the name is not important). Contents should look like this (consult the documentation for your jdbc driver for details):
<?xml version="1.0" encoding="UTF-8"?> <datasources> <local-tx-datasource> <jndi-name>jdbc/mydatasource</jndi-name> <connection-url>jdbc:.....connection url.......</connection-url> <driver-class>......jdbc driver class.......</driver-class> <user-name>..........</user-name> <password>.........</password> </local-tx-datasource> </datasources>
The Hibernate MBean
As per chapter 13 of the JBoss documentation, we create a .har archive (a Hibernate Archive), with the following directory structure:
- my-sessionfactory.har
- my-domainclasses.jar
- my-mapping1.hbm.xml
- my-mapping2.hbm.xml
- META-INF
- jboss-service.xml
The my-domainclasses.jar should contain your hibernate mapped domain beans. The hbm.xml mapping files are found automatically by the MBean, provided they are not inside a jar. The jboss-service.xml looks like this:
<server> <mbean code="org.jboss.hibernate.jmx.Hibernate" name="jboss.har:service=Hibernate"> <depends>jboss:service=Naming</depends> <depends>jboss:service=TransactionManager</depends> <attribute name="DatasourceName">java:jdbc/mydatasource</attribute> <attribute name="Dialect">org.hibernate.dialect.AnAppropriateSQLDialect</attribute> <attribute name="SessionFactoryName">java:/hibernate/SessionFactory</attribute> </mbean> </server>
this concludes the necessary JBoss setup. Now let's look at the trails application side.
Your Trails application in JBoss
include/exclude from your war.
Make sure your war archive does NOT contain the hibernate jars and jdbc driver, since they will already be present in JBoss (see above). your trails application could contain the jar holding your domain classes and must contain the hbm.xml mapping files.
The Spring configuration
The sessionFactory and dataSource beans should be configured differently, since we're using the objects provided by JBoss:
<bean id="sessionFactory" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"><value>java:/hibernate/SessionFactory</value></property> </bean> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
Patching Trails
You would think that the configuration above is all we need, after all, this is what makes Spring so cool: swap out your LocalSessionFactoryBean for a JndiFactoryBean in the config and everything just works. Unfortunately, Trails uses a method that is specific to =LocalSessionFactoryBean=, which we don't use here. The code that uses this method is located in a single class: HibernateDescriptorDecorator. This class uses LocalSessionFactoryBean's getConfiguration() method to access the mapping objects (java representations of the .hbm.xml files).
By modifying this class, we add the possibility of providing .hbm.xml files in the Spring configuration while maintaining backwards compatibility. The spring configuration now looks like this:
<bean id="hibernateDescriptorDecorator" class="org.trails.hibernate.jboss.HibernateJbossDescriptorDecorator" singleton="true"> <property name="mappingResourceNames"> <list> <value>my-mapping1.hbm.xml</value> <value>my-mapping2.hbm.xml</value> </list> </property> <property name="sessionFactory"><ref local="sessionFactory"/></property> </bean>
The mapping files should be present in your application, in the root of your classpath in the example above. The new implementation of HibernateDescriptorDecorator, based on trails 0.8 code, is listed at the end of this article.
One last thing
Unfortunately, our changes still don't produce a working application. There is a problem with the trails class TrailsDescriptorService. The trailsDescriptorService holds a Map from (mapped) classes to descriptors. The keys for this map are Class objects. The descriptor map is initialized with all mapped classes found in the hibernate mapping files. The getClassDescriptor method does a map lookup with a Class object.
So, for example, a descriptor is placed in the map with Class object class my.example.Foo as the key and a IClassDescriptor as its value. When you call getClassDescriptor with a Class object class my.example.Foo, you would think that you'd get your class descriptor, right? wrong! Probably due to class loader issues, both Class objects are not equal. I have changed the TrailsDescriptorService code to use someClass.getName() as the key for the descriptor map. Code is provided below.
Concluding remarks
This concludes our efforts to get Trails working with JBoss. Please note that this code isn't really thoroughly tested yet, and some issues remain: Trails adds some event listeners to the hibernate sessionfactory that are not present in our shared hibernate mbean. We're still looking into this problem.
Listing: HibernateJbossDescriptorDecorator
This is our new HibernateDescriptorDecorator. It is backwards compatible with trails 0.8 code (a localSessionFactoryBean property will still be used if present in the applicationContext.xml) but adds the possibility of providing mapping files in a list.
package org.trails.hibernate.jboss; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.management.MBeanServer; import javax.management.MBeanServerFactory; import ognl.Ognl; import ognl.OgnlException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.HibernateException; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; import org.hibernate.mapping.Column; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; import org.hibernate.mapping.SimpleValue; import org.hibernate.metadata.ClassMetadata; import org.hibernate.metadata.CollectionMetadata; import org.hibernate.type.AssociationType; import org.hibernate.type.Type; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContextException; import org.springframework.orm.hibernate3.LocalSessionFactoryBean; import org.trails.TrailsRuntimeException; import org.trails.descriptor.CollectionDescriptor; import org.trails.descriptor.DescriptorDecorator; import org.trails.descriptor.IClassDescriptor; import org.trails.descriptor.IDescriptorFactory; import org.trails.descriptor.IPropertyDescriptor; import org.trails.descriptor.IdentifierDescriptor; import org.trails.descriptor.ObjectReferenceDescriptor; import org.trails.hibernate.HibernateDescriptorDecorator; import org.trails.hibernate.MetadataNotFoundException; public class HibernateJbossDescriptorDecorator implements DescriptorDecorator, InitializingBean { private static Log log = LogFactory.getLog(HibernateDescriptorDecorator.class); private List<String> mappingResourceNames; private LocalSessionFactoryBean localSessionFactoryBean; private SessionFactory sessionFactory; private Configuration hibernateConfiguration; private List types; private IDescriptorFactory descriptorFactory; private HashMap descriptors = new HashMap(); private int largeColumnLength = 100; /** * The default way to order our property descriptors is by the * order they appear in the hibernate config, with id first. Any non-mapped * properties are tacked on at the end, til I think of a better way. * @param propertyDescriptors * @return */ protected List sortPropertyDescriptors(Class type, List propertyDescriptors) { ArrayList sortedPropertyDescriptors = new ArrayList(); try { sortedPropertyDescriptors.add(Ognl.getValue("#this.{? identifier == true}[0]", propertyDescriptors)); for (Iterator iter = getMapping(type).getPropertyIterator(); iter .hasNext();) { Property mapping = (Property) iter.next(); sortedPropertyDescriptors.addAll((List)Ognl.getValue("#this.{ ? name == \"" + mapping.getName() + "\"}", propertyDescriptors)); } } catch(Exception ex) { throw new TrailsRuntimeException(ex); } return sortedPropertyDescriptors; } /** * Find the Hibernate metadata for this type, traversing up * the hierarchy to supertypes if necessary * @param type * @return */ protected ClassMetadata findMetadata(Class type) throws MetadataNotFoundException { ClassMetadata metaData = getSessionFactory().getClassMetadata(type); if (metaData != null ) return metaData; if (metaData == null && !type.equals(Object.class)) { return findMetadata(type.getSuperclass()); } else throw new MetadataNotFoundException("Failed to find metadata."); } /** * @param type The class of the descriptor containing this property * @param descriptor the descriptor to decorate */ protected IPropertyDescriptor decoratePropertyDescriptor(Class type, IPropertyDescriptor descriptor) { try { ClassMetadata classMetaData = findMetadata(type); if (descriptor.getName().equals(getIdentifierProperty(type))) { return buildIdentifierDescriptor(type, descriptor); } if (notAHibernateProperty(classMetaData, descriptor)) { return descriptor; } Property mappingProperty = getMapping(type).getProperty(descriptor.getName()); descriptor.setLength(findColumnLength(mappingProperty)); descriptor.setLarge(isLarge(mappingProperty)); if (!mappingProperty.isOptional()) { descriptor.setRequired(true); } if (!mappingProperty.isInsertable() && !mappingProperty.isUpdateable()) { descriptor.setReadOnly(true); } Type hibernateType = classMetaData.getPropertyType(descriptor.getName()); if (Collection.class.isAssignableFrom(descriptor.getPropertyType())) { return buildCollectionDescriptor(type, descriptor); }else if (hibernateType.isAssociationType()) { return buildReferenceDescriptor(descriptor, (AssociationType) hibernateType); } }catch (HibernateException e) { throw new TrailsRuntimeException(e); } return descriptor; } private boolean isLarge(Property mappingProperty) { // Hack to avoid setting large property if length // is exactly equal to Hibernate default column length return findColumnLength(mappingProperty) != Column.DEFAULT_LENGTH && findColumnLength(mappingProperty) > getLargeColumnLength(); } private int findColumnLength(Property mappingProperty) { int length = 0; for (Iterator iter = mappingProperty.getColumnIterator(); iter.hasNext();) { Column column = (Column) iter.next(); length += column.getLength(); } return length; } /** * @param classMetaData * @param type * @return */ protected boolean notAHibernateProperty(ClassMetadata classMetaData, IPropertyDescriptor descriptor) { try { return ((Boolean)Ognl.getValue("propertyNames.{ ? #this {{= \"" + descriptor.getName() + "\"}.size() =}} 0", classMetaData)).booleanValue(); } catch (OgnlException oe) { throw new TrailsRuntimeException(oe); } } /** * @param descriptor * @param type * @return */ private IPropertyDescriptor buildReferenceDescriptor( IPropertyDescriptor descriptor, AssociationType type) { return new ObjectReferenceDescriptor(descriptor, type.getReturnedClass()); } /** * @param type * @param descriptor * @return */ private IdentifierDescriptor buildIdentifierDescriptor(Class type, IPropertyDescriptor descriptor) { IdentifierDescriptor identifierDescriptor = new IdentifierDescriptor(descriptor); PersistentClass mapping = getMapping(type); if (((SimpleValue)mapping.getIdentifier()).getIdentifierGeneratorStrategy().equals("assigned")) { identifierDescriptor.setGenerated(false); } return identifierDescriptor; } /** * @param type * @return */ protected PersistentClass getMapping(Class type) { Configuration cfg = getHibernateConfiguration(); PersistentClass mapping = cfg.getClassMapping(type.getName()); return mapping; } /** * @param type * @param newDescriptor */ private CollectionDescriptor buildCollectionDescriptor(Class type, IPropertyDescriptor descriptor) { try { Map allCollectionMeta = getSessionFactory().getAllCollectionMetadata(); CollectionDescriptor collectionDescriptor = new CollectionDescriptor(descriptor); org.hibernate.mapping.Collection collectionMapping = findCollectionMapping(type, descriptor.getName()); // It is a child relationship if it has delete-orphan specified in the mapping collectionDescriptor.setChildRelationship(collectionMapping.hasOrphanDelete()); CollectionMetadata collectionMetaData = getSessionFactory() .getCollectionMetadata(collectionMapping.getRole()); collectionDescriptor.setElementType(collectionMetaData.getElementType() .getReturnedClass()); return collectionDescriptor; }catch (HibernateException e) { throw new TrailsRuntimeException(e); } } protected org.hibernate.mapping.Collection findCollectionMapping(Class type, String name) { String roleName = type.getName() + "." + name; org.hibernate.mapping.Collection collectionMapping = getHibernateConfiguration().getCollectionMapping(roleName); if (collectionMapping != null) { return collectionMapping; } else if (!type.equals(Object.class)) { return findCollectionMapping(type.getSuperclass(), name); } else { throw new MetadataNotFoundException("Metadata not found."); } } /* (non-Javadoc) * @see org.trails.descriptor.PropertyDescriptorService#getIdentifierProperty(java.lang.Class) */ public String getIdentifierProperty(Class type) { try { Map allMeta = getSessionFactory().getAllClassMetadata(); return getSessionFactory().getClassMetadata(type) .getIdentifierPropertyName(); }catch (HibernateException e) { throw new TrailsRuntimeException(e); } } public IClassDescriptor getClassDescriptor(Class type) { return (IClassDescriptor)descriptors.get(type); } /* (non-Javadoc) * @see org.trails.descriptor.TrailsDescriptorService#getAllDescriptors() */ public List getAllDescriptors() { return new ArrayList(descriptors.values()); } public IDescriptorFactory getDescriptorFactory() { return descriptorFactory; } public void setDescriptorFactory(IDescriptorFactory descriptorFactory) { this.descriptorFactory = descriptorFactory; } public List getTypes() { return types; } public void setTypes(List types) { this.types = types; } public IClassDescriptor decorate(IClassDescriptor descriptor) { ArrayList decoratedPropertyDescriptors = new ArrayList(); for (Iterator iter = descriptor.getPropertyDescriptors().iterator(); iter.hasNext();) { IPropertyDescriptor propertyDescriptor = (IPropertyDescriptor) iter.next(); decoratedPropertyDescriptors.add(decoratePropertyDescriptor( descriptor.getType(), propertyDescriptor)); } descriptor.setPropertyDescriptors(decoratedPropertyDescriptors); return descriptor; } public int getLargeColumnLength() { return largeColumnLength; } /** * Columns longer than this will have their large property set * to true. * @param largeColumnLength */ public void setLargeColumnLength(int largeColumnLength) { this.largeColumnLength = largeColumnLength; } public void afterPropertiesSet() throws Exception { if (sessionFactory {{= null) { if (localSessionFactoryBean !}} null) { sessionFactory = (SessionFactory) localSessionFactoryBean.getObject(); } else { throw new ApplicationContextException(getClass() + " needs either a sessionFactory or a localSessionFactoryBean."); } } if (localSessionFactoryBean != null) { // configuration comes from localSessionFactoryBean log.info("Configuring decorator using localSessionFactoryBean"); hibernateConfiguration = localSessionFactoryBean.getConfiguration(); } else if (mappingResourceNames != null) { hibernateConfiguration = new Configuration(); // configure using resource names. log.info("Configuring decorator using mappingResourceNames"); for (String resource : mappingResourceNames) { URL rsrc = getClass().getClassLoader().getResource(resource); log.info("found resource with URL=" + rsrc); hibernateConfiguration.addResource(resource); } } else if (hibernateConfiguration == null) { // if no other configuration method is used, and the // configuration isn't set by spring, throw an exception. throw new ApplicationContextException(getClass() + " needs either a hibernateConfiguration, mappingResourceNames or localSessionFactoryBean"); } } public Configuration getHibernateConfiguration() { return hibernateConfiguration; } public void setHibernateConfiguration(Configuration hibernateConfiguration) { this.hibernateConfiguration = hibernateConfiguration; } public LocalSessionFactoryBean getLocalSessionFactoryBean() { return localSessionFactoryBean; } public void setLocalSessionFactoryBean( LocalSessionFactoryBean localSessionFactoryBean) { this.localSessionFactoryBean = localSessionFactoryBean; } public List<String> getMappingResourceNames() { return mappingResourceNames; } public void setMappingResourceNames(List<String> mappingResourceNames) { this.mappingResourceNames = mappingResourceNames; } public SessionFactory getSessionFactory() { return sessionFactory; } public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } }
Listing: TrailsDescriptorService
package org.trails.descriptor; import java.beans.BeanDescriptor; import java.beans.BeanInfo; import java.beans.FeatureDescriptor; import java.beans.Introspector; import java.beans.MethodDescriptor; import java.beans.PropertyDescriptor; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import ognl.Ognl; import ognl.OgnlException; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.trails.persistence.PersistenceService; public class TrailsDescriptorService implements DescriptorService { private static Log log = LogFactory.getLog(PersistenceService.class); protected List types; protected Map descriptors = new HashMap(); private List decorators = new ArrayList(); private List propertyExcludes = new ArrayList(); private List methodExcludes = new ArrayList(); public class DescriptorComparator implements Comparator { public int compare(Object o1, Object o2) { IClassDescriptor descriptor1 = (IClassDescriptor)o1; IClassDescriptor descriptor2 = (IClassDescriptor)o2; return descriptor1.getDisplayName().compareTo( descriptor2.getDisplayName()); } } public void init() throws OgnlException { descriptors.clear(); for (Iterator iter = types.iterator(); iter.hasNext();) { Class type = (Class) iter.next(); descriptors.put(type.getName(), applyDecorators(buildClassDescriptor(type))); } // second pass to find children markChildClasses(); } /** * Have the decorators decorate this descriptor * @param descriptor * @return The resulting descriptor after all decorators are applied */ protected IClassDescriptor applyDecorators(IClassDescriptor descriptor) { IClassDescriptor currDescriptor = descriptor; for (Iterator iter = getDecorators().iterator(); iter.hasNext();) { DescriptorDecorator decorator = (DescriptorDecorator) iter.next(); currDescriptor = decorator.decorate(currDescriptor); } return currDescriptor; } protected void markChildClasses() throws OgnlException { List childRelationships = (List)Ognl.getValue("#root.{ propertyDescriptors }", descriptors); for (Iterator iter = childRelationships.iterator(); iter.hasNext();) { List list = (List)iter.next(); for (Iterator iterator = list.iterator(); iterator.hasNext();) { TrailsPropertyDescriptor propertyDescriptor = (TrailsPropertyDescriptor) iterator.next(); if (propertyDescriptor.isCollection() && ((CollectionDescriptor)propertyDescriptor).isChildRelationship()) { getClassDescriptor(((CollectionDescriptor)propertyDescriptor).getElementType()).setChild(true); } } } } /* (non-Javadoc) * @see org.trails.descriptor.IDescriptorFactory#buildClassDescriptor(java.lang.Class) */ public IClassDescriptor getClassDescriptor(Class type) { IClassDescriptor descriptor; String className; if (type.getName().contains("CGLIB")) { className = type.getSuperclass().getName(); } else { className = type.getName(); } if (type.getName().contains("CGLIB")) { descriptor = (IClassDescriptor)descriptors.get(className); log.debug("getClassDescriptor: found CGLIB descriptor " + descriptor + " for " + className); } else { descriptor = (IClassDescriptor)descriptors.get(className); log.debug("getClassDescriptor: found descriptor " + descriptor + " for " + className); } return descriptor; } protected IClassDescriptor buildClassDescriptor(Class type) { try { TrailsClassDescriptor descriptor = new TrailsClassDescriptor(type); BeanInfo beanInfo = Introspector.getBeanInfo(type); BeanUtils.copyProperties(descriptor, beanInfo.getBeanDescriptor()); descriptor.setPropertyDescriptors(buildPropertyDescriptors(beanInfo)); //descriptor.setMethodDescriptors(buildMethodDescriptors(beanInfo)); return descriptor; } catch (Exception ex) { ex.printStackTrace(); } return null; } // protected List buildMethodDescriptors(BeanInfo beanInfo) // { // ArrayList methodDescriptors = new ArrayList(); // for (int i = 0; i < beanInfo.getMethodDescriptors().length; i++) // { // MethodDescriptor methodDescriptor = beanInfo.getMethodDescriptors()[i]; // methodDescriptors.add(new TrailsMethodDescriptor( // methodDescriptor.getName(), methodDescriptor.getMethod().getParameterTypes())); // } // return methodDescriptors; // } protected List buildPropertyDescriptors(BeanInfo beanInfo) throws Exception { ArrayList propertyDescriptors = new ArrayList(); for (int i = 0; i < beanInfo.getPropertyDescriptors().length; i++) { PropertyDescriptor beanPropDescriptor = beanInfo.getPropertyDescriptors()[i]; if (!isExcluded(beanPropDescriptor.getName(), getPropertyExcludes())) { TrailsPropertyDescriptor propDescriptor = new TrailsPropertyDescriptor(beanPropDescriptor.getPropertyType()); BeanUtils.copyProperties(propDescriptor, beanPropDescriptor); propertyDescriptors.add(propDescriptor); } } return propertyDescriptors; } public List getTypes() { return types; } /** * * @param types all the classes this service should describe */ public void setTypes(List types) { this.types = types; } public List getAllDescriptors() { List allDescriptors = new ArrayList(descriptors.values()); Collections.sort( allDescriptors, new DescriptorComparator()); return allDescriptors; } public List getDecorators() { return decorators; } public void setDecorators(List decorators) { this.decorators = decorators; } public List getMethodExcludes() { return methodExcludes; } public void setMethodExcludes(List methodExcludes) { this.methodExcludes = methodExcludes; } public List getPropertyExcludes() { return propertyExcludes; } public void setPropertyExcludes(List propertyExcludes) { this.propertyExcludes = propertyExcludes; } /** * @param methodDescriptor * @param excludes * @return */ protected boolean isExcluded(String name, List excludes) { for (Iterator iter = excludes.iterator(); iter.hasNext();) { String exclude = (String) iter.next(); if (name.matches(exclude)) { return true; } } return false; } }
