package org.gmod.schema.cfg; import org.gmod.schema.mapped.Feature; import org.apache.log4j.Logger; import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.SingleTableSubclass; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.util.ClassUtils; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.persistence.Entity; import javax.persistence.MappedSuperclass; import javax.sql.DataSource; /** * Extends org.hibernate.cfg.AnnotationConfiguration in two ways: * <ul> * <li> Adds support for the {@link FeatureType} annotation. * <li> Extends the behaviour of {@link #addPackage} to add all annotated classes in the package. * </ul> * If you use this directly, you must set the data source. (In that case you may * also want to use the <code>InjectedDataSourceConnectionProvider</code>; see * {@link ChadoAnnotationSettingsFactory} for more information.) * From Spring, use {@link ChadoSessionFactoryBean} * instead, which defaults to this configuration and automatically sets the data source. * * @author rh11 */ @SuppressWarnings("serial") public class ChadoAnnotationConfiguration extends AnnotationConfiguration { private static final Logger logger = Logger.getLogger(ChadoAnnotationConfiguration.class); private DataSource dataSource; public ChadoAnnotationConfiguration() { super(new ChadoAnnotationSettingsFactory()); /* * Why use ChadoAnnotationSettingsFactory here? * * The ChadoAnnotationConfiguration needs access to the database, so it can * look up the CV terms that are referenced in the @FeatureType annotation, * and set the discriminator value to the appropriate term ID. * * If you use Hibernate via Spring, then Spring will use its * LocalDatasourceConnectionProvider to provide database connections to Hibernate * from the DataSource supplied to the SessionFactoryBean; the ChadoSessionFactoryBean * additionally passes this DataSource object to the ChadoAnnotationConfiguration. * * The problem comes if you use ChadoAnnotationConfiguration not from Spring. * You need to supply a DataSource to the ChadoAnnotationConfiguration, but Hibernate * cannot use this DataSource to obtain its connections. (There is * a DataSourceConnectionProvider, but that requires the DataSource to be * registered with JNDI.) * * The natural solution is to define a new ConnectionProvider class which uses * a DataSource to provide connections (but doesn't rely on getting that object * from JNDI) and then to arrange for the ChadoAnnotationConfiguration to make * its DataSource available to this ConnectionProvider. The only way I can see * to do the second part is to define yet another class, this time extending * SettingsFactory, which the ChadoAnnotationConfiguration uses. This * SettingsFactory can then override the createConnectionProvider method and pass * the DataSource to the ConnectionProvider that it creates. * * Fortuitously it turns out that there is already a suitable ConnectionProvider * defined in the Hibernate EJB3 code, * org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider, so I've used * that rather than reimplement it. The new SettingsFactory is, obviously, * ChadoAnnotationSettingsFactory. * * In fact we don't even attempt to influence the choice of ConnectionProvider, * because Spring (for example) still needs to be free to make its own choices. * The ChadoAnnotationSettingsFactory simply checks which connection provider is * being used, and injects the DataSource only if it finds an * InjectedDataSourceConnectionProvider. */ } public ChadoAnnotationConfiguration setDataSource(DataSource dataSource) { this.dataSource = dataSource; ((ChadoAnnotationSettingsFactory) settingsFactory).setDataSource(dataSource); return this; } private ClassLoader classLoader = getClass().getClassLoader(); /** * Set the class loader to use for locating the classes within a * package, when <code>addPackage</code> is called. The default, * if this method is not called, is to use the class loader that was * used to load this class. * * @param classLoader */ public void setClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } private Map<Class<? extends Feature>, Set<Integer>> typeIdsByClass = new HashMap<Class<? extends Feature>, Set<Integer>>(); private void recordTypeId(int typeId, Class<?> mappedClass) { if (!Feature.class.isAssignableFrom(mappedClass)) { throw new RuntimeException(String.format( "Class '%s' has a @FeatureType annotation, but is not a subclass of Feature", mappedClass)); } Class<? extends Feature> featureClass = mappedClass.asSubclass(Feature.class); boolean finished = false; do { logger.trace(String.format("Class '%s' can be represented by CV term %d", featureClass, typeId)); if (!typeIdsByClass.containsKey(featureClass)) { typeIdsByClass.put(featureClass, new HashSet<Integer>()); } typeIdsByClass.get(featureClass).add(typeId); Class<?> superclass = featureClass.getSuperclass(); if (superclass != null && Feature.class.isAssignableFrom(superclass)) { featureClass = superclass.asSubclass(Feature.class); } else { finished = true; } } while (!finished); } /** * Get the type IDs that represent this class or a subclass. * * @param featureClass the class of Feature * @return a collection of CvTerm IDs */ public Collection<Integer> getTypeIdsByClass(Class<? extends Feature> featureClass) { if (!typeIdsByClass.containsKey(featureClass)) { throw new RuntimeException(String.format( "Neither the Feature class '%s', nor any of its subclasses, has a @FeatureType annotation", featureClass)); } return typeIdsByClass.get(featureClass); } /** * Adds the specified package, reading any package metadata, and * <b>also</b> adds all annotated classes from this package. */ @Override public AnnotationConfiguration addPackage(String packageName) throws MappingException { logger.info("Adding package: " + packageName); addClassesFromAnnotatedPackage(packageName); super.addPackage(packageName); return this; } private void addClassesFromAnnotatedPackage(String packageName) { ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); scanner.addIncludeFilter( new AnnotationTypeFilter(Entity.class) ); scanner.addIncludeFilter( new AnnotationTypeFilter(MappedSuperclass.class) ); for (BeanDefinition bd : scanner.findCandidateComponents(packageName)) { String className = bd.getBeanClassName(); Class<?> type = ClassUtils.resolveClassName(className, this.classLoader); addAnnotatedClass(type); logger.info(String.format("Added class '%s'", className)); } } @Override public SessionFactory buildSessionFactory() throws HibernateException { logger.debug("Building session factory"); buildMappings(); /* Not quite as redundant as it looks: the <code>classes</code> * field is declared as a plain old non-generic Map. */ @SuppressWarnings("unchecked") Map<String,PersistentClass> classes = this.classes; for (Map.Entry<String,PersistentClass> e: classes.entrySet()) { String className = e.getKey(); PersistentClass persistentClass = e.getValue(); logger.trace(String.format("Inspecting class '%s'", className)); if (!(persistentClass instanceof SingleTableSubclass)) { continue; } logger.trace(String.format("Processing class '%s'", className)); Class<?> mappedClass = persistentClass.getMappedClass(); FeatureType featureType = FeatureTypeUtils.getFeatureTypeForClass(mappedClass); if (featureType != null) { Integer cvTermId = getCvTermIdForFeatureType(featureType); if (cvTermId == null) { throw new HibernateException(String.format("Failed to initialise class '%s': could not find %s", className, description(featureType))); } logger.debug(String.format("Setting discriminator column of '%s' to %d (for %s)", className, cvTermId, description(featureType))); persistentClass.setDiscriminatorValue(String.valueOf(cvTermId)); recordTypeId(cvTermId, mappedClass); } } return super.buildSessionFactory(); } private String description(FeatureType featureType) { if ("".equals(featureType.accession())) { return String.format("term '%s' in CV '%s'", featureType.term(), featureType.cv()); } return String.format("accession number '%s' in CV '%s'", featureType.accession(), featureType.cv()); } private Integer getCvTermIdForFeatureType(FeatureType featureType) throws ChadoAnnotationException { if ("".equals(featureType.accession())) { return getCvTermIdForTermFeatureType(featureType); } return getCvTermIdForAccessionFeatureType(featureType); } private Integer getCvTermIdForTermFeatureType(FeatureType featureType) throws ChadoAnnotationException { try { Connection conn = dataSource.getConnection(); PreparedStatement st = conn.prepareStatement( "select cvterm_id" + " from cvterm" + " join cv on cvterm.cv_id = cv.cv_id" + " where cv.name = ?" + " and cvterm.name = ?"); try { st.setString(1, featureType.cv()); st.setString(2, featureType.term()); ResultSet rs = st.executeQuery(); if (!rs.next()) { return null; } return rs.getInt(1); } finally { try { st.close(); conn.close(); } catch (SQLException e) { logger.error(e); } } } catch (SQLException e) { throw new ChadoAnnotationException(e); } } private Integer getCvTermIdForAccessionFeatureType(FeatureType featureType) throws ChadoAnnotationException { try { Connection conn = dataSource.getConnection(); PreparedStatement st = conn.prepareStatement( "select cvterm_id" + " from cvterm" + " join cv on cvterm.cv_id = cv.cv_id" + " join dbxref on cvterm.dbxref_id = dbxref.dbxref_id" + " where cv.name = ?" + " and dbxref.accession = ?"); try { st.setString(1, featureType.cv()); st.setString(2, featureType.accession()); ResultSet rs = st.executeQuery(); if (!rs.next()) { return null; } return rs.getInt(1); } finally { try { st.close(); conn.close(); } catch (SQLException e) { logger.error(e); } } } catch (SQLException e) { throw new ChadoAnnotationException(e); } } }