Java tutorial
/* * Copyright 2001-2012 Remi Vankeisbelck * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package woko.hibernate; import net.sourceforge.stripes.util.ReflectUtil; import net.sourceforge.stripes.util.ResolverUtil; import org.hibernate.*; import org.hibernate.annotations.Entity; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.cfg.Configuration; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.hibernate.metadata.ClassMetadata; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import woko.Closeable; import woko.persistence.*; import woko.util.Util; import woko.util.WLogger; import java.beans.PropertyDescriptor; import java.io.Serializable; import java.lang.reflect.Method; import java.net.URL; import java.text.Normalizer; import java.util.*; /** * Hibernate-backed <code>ObjectStore</code> implementation. Uses Hibernate and JPA to persist the * Woko-managed POJOs. */ public class HibernateStore implements ObjectStore, TransactionalStore, Closeable { public static final String DEFAULT_HIBERNATE_CFG_XML = "/woko_default_hibernate.cfg.xml"; public static final String CTX_PARAM_PACKAGE_NAMES = "Woko.Hibernate.Packages"; private static final WLogger log = WLogger.getLogger(HibernateStore.class); private HibernatePrimaryKeyConverter primaryKeyConverter; private SessionFactory sessionFactory; private static final AlternateKeyConverter DEFAULT_ALTERNATE_KEY_CONVERTER = new DefaultAlternateKeyConverter(); private List<Class<?>> mappedClasses; protected final List<String> packageNames; /** * Create the store with passed packages. Will look for <code>@Entity</code>-annotated classes in specified * packaged. * If no <code>hibernate.cfg.xml</code> is supplied, will default to * <code>woko_default_hibernate.cfg.xml</code>. Simply place a <code>hibernate.cfg.xml</code> to your CLASSPATH * root in order to customize the session factory details. * @param packageNames a list of packages to scan entities for */ public HibernateStore(List<String> packageNames) { this(packageNames, true); } public HibernateStore(List<String> packageNames, boolean initialize) { this.packageNames = packageNames; if (initialize) { initialize(); } } protected void initialize() { log.info("Creating with package names : " + packageNames); Configuration cfg = configure(createConfiguration(packageNames)); log.info("Configuration created, building session factory..."); sessionFactory = cfg.buildSessionFactory(); log.info("Created session factory : " + sessionFactory); primaryKeyConverter = createPrimaryKeyConverter(); log.info("Created PK converter : " + primaryKeyConverter); int nbClasses = mappedClasses.size(); if (nbClasses > 0) { log.info(nbClasses + " persistent class(es) added."); } else { log.warn("No mapped classes found for packages " + packageNames + ". Make sure your @Entity classes are in these packages."); } } /** * Configures hibernate. This implementation checks for /hibernate.cfg.xml and default hb config * file. Can be overriden in order to configure hb another way. * @param config the freshly created config * @return the config */ protected Configuration configure(Configuration config) { log.info("Configuration created, building session factory..."); URL u = getClass().getResource("/hibernate.cfg.xml"); if (u == null) { String hbCfg = getDefaultHibernateCfgXml(); log.warn("Using default hibernate settings from " + hbCfg + " : in-memory hsql. Add your own hibernate.cfg.xml to the classpath to change the settings."); u = getClass().getResource(hbCfg); } return config.configure(u); } /** * Return the default <code>woko_default_hibernate.cfg.xml</code> session factory configuration file * @return the default session factory config */ protected String getDefaultHibernateCfgXml() { return DEFAULT_HIBERNATE_CFG_XML; } /** * Return the hibernate Session Factory * @return the Session Factory */ public SessionFactory getSessionFactory() { return sessionFactory; } /** * Create and return the primary key converter * @return the primary key converter */ protected HibernatePrimaryKeyConverter createPrimaryKeyConverter() { return new HibernatePrimaryKeyConverter(); } /** * Create the Hibernate <code>Configuration</code> for specified packages * @param packageNames the packages to scan for entities * @return the Hibernate Configuration */ protected Configuration createConfiguration(List<String> packageNames) { mappedClasses = new ArrayList<Class<?>>(); if (packageNames == null) { packageNames = Arrays.asList("model"); } log.info("Creating hibernate annotation configuration"); AnnotationConfiguration cfg = new AnnotationConfiguration(); ResolverUtil<Object> resolverUtil = new ResolverUtil<Object>(); String[] packages = new String[packageNames.size()]; packages = packageNames.toArray(packages); resolverUtil.findAnnotated(Entity.class, packages); resolverUtil.findAnnotated(javax.persistence.Entity.class, packages); for (Class<?> clazz : resolverUtil.getClasses()) { cfg.addAnnotatedClass(clazz); mappedClasses.add(clazz); log.info(" * " + clazz + " added to config"); } Collections.sort(mappedClasses, new Comparator<Class<?>>() { @Override public int compare(Class<?> aClass, Class<?> aClass1) { return aClass.getSimpleName().compareTo(aClass1.getSimpleName()); } }); return cfg; } /** * Return the current Hibernate Session * @return the current Hibernate Session */ public Session getSession() { return sessionFactory.getCurrentSession(); } protected Object loadObjectWithAlternateKey(Class<?> mappedClass, Util.PropertyNameAndAnnotation<WokoAlternateKey> propAndAltKey, String keyValue) { Util.assertArg("mappedClass", mappedClass); Util.assertArg("propAndAltKey", propAndAltKey); Util.assertArg("keyValue", keyValue); WokoAlternateKey alternateKey = propAndAltKey.getAnnotation(); try { return getSession().createCriteria(mappedClass) .add(Restrictions.eq(alternateKey.altKeyProperty(), keyValue)).setCacheable(true) .uniqueResult(); } catch (NonUniqueResultException e) { // ouch ! several entities with the same altKey... String msg = "More than 1 entity found for class " + mappedClass + " with alternate key " + propAndAltKey; log.error(msg, e); throw new RuntimeException(msg, e); } } private Util.PropertyNameAndAnnotation<WokoAlternateKey> checkForAlternateKey(Class<?> mappedClass) { return Util.findAnnotationOnFieldOrAccessor(mappedClass, WokoAlternateKey.class); } /** * Load persistent object from the database using the current Hibernate Session. * @param className the (mapped) class name of the object to load * @param key the key (ID) of the object to load * @return the object if found, <code>null</code> otherwise */ public Object load(String className, String key) { log.debug("Loading object for className " + className + ", key=" + key); if (className == null && key == null) { return null; } Class<?> mappedClass = getMappedClass(className); if (mappedClass == null) { return null; } if (key == null) { return null; } Class<?> keyType = getPrimaryKeyClass(mappedClass); if (keyType == null) { log.warn("Unable to get key type for mappedClass " + mappedClass); return null; } Session s = getSession(); Transaction tx = s.getTransaction(); if (log.isDebugEnabled()) { log.debug("Using transaction " + tx); } // try actual object ID first... Serializable id = primaryKeyConverter.convert(key, keyType); if (id == null) { if (log.isDebugEnabled()) { log.debug("Converted key " + key + " to null, will try alternate key..."); } } else { Object o = s.get(mappedClass, id); if (o != null) { return o; } } // introspect mapped class and check for @WokoAlternateKey Util.PropertyNameAndAnnotation<WokoAlternateKey> alternateKey = checkForAlternateKey(mappedClass); if (alternateKey != null) { if (log.isDebugEnabled()) { log.debug( "@WokoAlternateKey found for mapped class " + mappedClass.getName() + ", querying object."); } // alternate key found, use this to grab the object Object o = loadObjectWithAlternateKey(mappedClass, alternateKey, key); if (o != null) { if (log.isDebugEnabled()) { log.debug("Found object for " + mappedClass.getName() + " with alternate key property " + alternateKey); } return o; } else { if (log.isDebugEnabled()) { log.debug("Object not found for " + mappedClass.getName() + " using alternate key property " + alternateKey + ", will try with ID"); } } } // no object to load ! log.warn("Unable to load any object for mapped class " + mappedClass + " and key " + key + ". Will return null."); return null; } /** * Save or update passed object (<code>Session.saveOrUpdate()</code>) * @param obj a Woko-managed POJO * @return the saved object */ public Object save(Object obj) { if (obj == null) { return null; } // alternate key management : we need to set the alternate key on save if // @WokoAlternateKey is used Class<?> clazz = obj.getClass(); Util.PropertyNameAndAnnotation<WokoAlternateKey> alternateKey = checkForAlternateKey(clazz); if (alternateKey != null) { Object alternateKeyValue = computeAlternateKeyValue(obj, alternateKey); if (alternateKeyValue == null) { log.warn("Alternate key specified for class " + clazz + " : " + alternateKey + ", but value of the " + "alternateKeyProperty is null. @WokoAlternateKey won't be used for this class."); } else { WokoAlternateKey annot = alternateKey.getAnnotation(); PropertyDescriptor pd = ReflectUtil.getPropertyDescriptor(clazz, annot.altKeyProperty()); Method setter = pd.getWriteMethod(); if (setter == null) { throw new IllegalStateException( "No setter found for alternate key property, could not set alternate key property for class " + clazz + ", alternateKey=" + alternateKey); } try { setter.invoke(obj, alternateKeyValue); } catch (Exception e) { log.error( "Exception caught while setting alternate key property, could not set alternate key property for class " + clazz + ", alternateKey=" + alternateKey); throw new RuntimeException(e); } } } getSession().saveOrUpdate(obj); return obj; } protected String sanitizeAlternateKey(String altKey) { if (altKey == null) { return null; } String key = (String) altKey; key = Normalizer.normalize(key, Normalizer.Form.NFD); key = key.replaceAll("[^\\p{ASCII}]", ""); key = key.replaceAll("\\s", "-"); return key; } protected Object computeAlternateKeyValue(Object obj, Util.PropertyNameAndAnnotation<WokoAlternateKey> propAndAnnot) { Util.assertArg("obj", obj); Util.assertArg("propAndAnnot", propAndAnnot); String propName = propAndAnnot.getPropertyName(); WokoAlternateKey annot = propAndAnnot.getAnnotation(); Object propValue = Util.getPropertyValue(obj, propName); if (propValue == null) { // property value is null ! should not happen as the // alternateKey-annoted props should always be not null // in this case, we just return null, so that alt key is not // used return null; } Class<? extends AlternateKeyConverter> converterClass = annot.converter(); AlternateKeyConverter converter; if (converterClass == null) { converter = DEFAULT_ALTERNATE_KEY_CONVERTER; } else { try { converter = converterClass.newInstance(); } catch (Exception e) { String msg = "Could not instanciate converter from class " + converterClass; log.error(msg, e); throw new RuntimeException(e); } } // check unicity of the generated key // and compute unique one if needed String altKey = sanitizeAlternateKey(converter.convert(obj, propAndAnnot, propName, propValue)); int counter = 1; Class<?> mappedClass = obj.getClass(); do { String uniqueKey = altKey; if (counter > 1) { uniqueKey = uniqueKey + "-" + counter; } List<?> matching = getSession().createCriteria(mappedClass).setCacheable(true) .add(Restrictions.eq(propName, uniqueKey)).list(); int nbMatching = matching.size(); if (nbMatching == 0 || (nbMatching == 1 && matching.get(0).equals(obj))) { return uniqueKey; } counter++; } while (counter < 1000); throw new IllegalStateException("Giving up computation of alternate key for class " + mappedClass + " and alternateKey " + propAndAnnot + ". All attempts to generate a key have failed."); } /** * Delete passed object (<code>Session.delete()</code>) * @param obj a Woko-managed POJO * @return the deleted object */ public Object delete(Object obj) { if (obj == null) { return null; } getSession().delete(obj); return obj; } /** * Uses primary key converter in order to get the key for passed object * @param obj a Woko-managed POJO * @return the key for passed object */ public String getKey(Object obj) { if (obj == null) { return null; } Class<?> mappedClass = getObjectClass(obj); // check for alternate key first... Util.PropertyNameAndAnnotation<WokoAlternateKey> altKey = checkForAlternateKey(mappedClass); if (altKey != null) { // grab the value of the alt key property String altKeyProp = altKey.getAnnotation().altKeyProperty(); Object altKeyVal = Util.getPropertyValue(obj, altKeyProp); if (altKeyVal != null) { // TODO what happens for non String props ??? return altKeyVal.toString(); } } // no alternate key was found, use the primary key Serializable k = primaryKeyConverter.getPrimaryKeyValue(sessionFactory, obj, mappedClass); if (k == null) { return null; } return k.toString(); } private Class<?> deproxify(Class<?> clazz) { // deproxify if needed String className = clazz.getName(); int i = className.indexOf("_$$_javassist"); if (i != -1) { className = className.substring(0, i); try { return Class.forName(className); } catch (ClassNotFoundException e) { log.error("Error while deproxifying " + clazz, e); throw new RuntimeException(e); } } return clazz; } public String getClassMapping(Class<?> clazz) { clazz = deproxify(clazz); // is the class persistent ? ClassMetadata classMetadata = sessionFactory.getClassMetadata(clazz); if (classMetadata == null) { // not a hibernated class, return the fully qualified class name return clazz.getName(); } else { // hibernated class, return the simple name return clazz.getSimpleName(); } } public Class<?> getMappedClass(String className) { if (className == null) { return null; } // lookup simple name in our list of mapped classes for (Class<?> clazz : mappedClasses) { String simpleName = clazz.getSimpleName(); if (simpleName.equals(className)) { return clazz; } } // try forName as a last resort try { return Class.forName(className); } catch (ClassNotFoundException e) { log.warn("Unable to load mapped class for " + className); return null; } } public Class<?> getPrimaryKeyClass(Class<?> entityClass) { ClassMetadata cm = sessionFactory.getClassMetadata(entityClass); if (cm == null) { // default to String return String.class; } else { return cm.getIdentifierType().getReturnedClass(); } } public ResultIterator<?> list(String className, Integer start, Integer limit) { Class clazz = getMappedClass(className); int s = start == null ? 0 : start; int l = limit == null ? -1 : limit; if (clazz == null) { return new ListResultIterator<Object>(Collections.emptyList(), s, l, 0); } else { Criteria crit = createListCriteria(clazz); // count crit.setProjection(Projections.rowCount()); Long count = (Long) crit.uniqueResult(); // sublist crit.setProjection(null); crit.setFirstResult(s); if (l != -1) { crit.setMaxResults(l); } // TODO optimize with scrollable results ? List<?> objects = crit.list(); return new ListResultIterator<Object>(objects, s, l, count.intValue()); } } protected Criteria createListCriteria(Class mappedClass) { return getSession().createCriteria(mappedClass); } public List<Class<?>> getMappedClasses() { return Collections.unmodifiableList(mappedClasses); } public ResultIterator search(Object query, Integer start, Integer limit) { throw new UnsupportedOperationException( "Search not implemented in HibernateStore. Override this method to handle full text search."); } public void close() { sessionFactory.close(); } /** * Execute passed callback in a Transaction and return the result * @param callback the callback to execute * @param <RES> the type of the result * @return the callback result * @deprecated use <code>doInTransactionWithResult</code> instead */ @Deprecated public <RES> RES doInTxWithResult(TxCallbackWithResult<RES> callback) { Session session = getSessionFactory().getCurrentSession(); Transaction tx = session.getTransaction(); if (tx == null || !tx.isActive()) { tx = session.beginTransaction(); } try { RES res = callback.execute(this, session); tx.commit(); return res; } catch (Exception e) { tx.rollback(); throw new RuntimeException(e); } finally { if (session.isOpen()) { session.close(); } } } /** * Execute passed callback in a Transaction * @param callback the callback to execute * @deprecated use <code>doInTransaction</code> instead */ @Deprecated public void doInTx(TxCallback callback) { Session session = getSessionFactory().getCurrentSession(); Transaction tx = session.getTransaction(); if (tx == null || !tx.isActive()) { tx = session.beginTransaction(); } try { callback.execute(this, session); tx.commit(); } catch (Exception e) { tx.rollback(); throw new RuntimeException(e); } finally { if (session.isOpen()) { session.close(); } } } @Override public <RES> RES doInTransactionWithResult(final TransactionCallbackWithResult<RES> callback) { Session session = getSessionFactory().getCurrentSession(); Transaction tx = session.getTransaction(); if (tx == null || !tx.isActive()) { tx = session.beginTransaction(); } try { RES res = callback.execute(); tx.commit(); return res; } catch (Exception e) { tx.rollback(); throw new RuntimeException(e); } finally { if (session.isOpen()) { session.close(); } } } @Override public void doInTransaction(final TransactionCallback callback) { Session session = getSessionFactory().getCurrentSession(); Transaction tx = session.getTransaction(); if (tx == null || !tx.isActive()) { tx = session.beginTransaction(); } try { callback.execute(); tx.commit(); } catch (Exception e) { tx.rollback(); throw new RuntimeException(e); } finally { if (session.isOpen()) { session.close(); } } } @Override public StoreTransaction getCurrentTransaction() { Session session = getSession(); Transaction tx = session.getTransaction(); if (tx == null) { return null; } return new HibernateStoreTransaction(tx); } @Override public StoreTransaction beginTransaction() { Session session = getSession(); Transaction tx = session.beginTransaction(); return new HibernateStoreTransaction(tx); } /** * Adapter for Hibernate Transactions. */ private class HibernateStoreTransaction implements StoreTransaction { private final Transaction hbTx; HibernateStoreTransaction(Transaction hbTx) { this.hbTx = hbTx; } @Override public boolean isActive() { return hbTx.isActive(); } @Override public void commit() { hbTx.commit(); } @Override public void rollback() { hbTx.rollback(); } } @Override public Class<?> getObjectClass(Object o) { return deproxyInstance(o).getClass(); } @SuppressWarnings("unchecked") protected <T> T deproxyInstance(T maybeProxy) { if (maybeProxy instanceof HibernateProxy) { HibernateProxy proxy = (HibernateProxy) maybeProxy; LazyInitializer i = proxy.getHibernateLazyInitializer(); return (T) i.getImplementation(); } return maybeProxy; } }