001/*
002 * Copyright 2002-2014 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.orm.hibernate4;
018
019import java.io.IOException;
020import java.lang.annotation.Annotation;
021import java.lang.reflect.Method;
022import java.util.Properties;
023import java.util.Set;
024import java.util.TreeSet;
025import javax.persistence.AttributeConverter;
026import javax.persistence.Embeddable;
027import javax.persistence.Entity;
028import javax.persistence.MappedSuperclass;
029import javax.sql.DataSource;
030import javax.transaction.TransactionManager;
031
032import org.hibernate.HibernateException;
033import org.hibernate.MappingException;
034import org.hibernate.SessionFactory;
035import org.hibernate.cache.spi.RegionFactory;
036import org.hibernate.cfg.AvailableSettings;
037import org.hibernate.cfg.Configuration;
038import org.hibernate.cfg.Environment;
039import org.hibernate.cfg.Settings;
040import org.hibernate.engine.transaction.internal.jta.CMTTransactionFactory;
041import org.hibernate.service.ServiceRegistry;
042
043import org.springframework.core.io.Resource;
044import org.springframework.core.io.ResourceLoader;
045import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
046import org.springframework.core.io.support.ResourcePatternResolver;
047import org.springframework.core.io.support.ResourcePatternUtils;
048import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
049import org.springframework.core.type.classreading.MetadataReader;
050import org.springframework.core.type.classreading.MetadataReaderFactory;
051import org.springframework.core.type.filter.AnnotationTypeFilter;
052import org.springframework.core.type.filter.TypeFilter;
053import org.springframework.transaction.jta.JtaTransactionManager;
054import org.springframework.util.Assert;
055import org.springframework.util.ClassUtils;
056
057/**
058 * A Spring-provided extension of the standard Hibernate {@link Configuration} class,
059 * adding {@link SpringSessionContext} as a default and providing convenient ways
060 * to specify a DataSource and an application class loader.
061 *
062 * <p>This is designed for programmatic use, e.g. in {@code @Bean} factory methods.
063 * Consider using {@link LocalSessionFactoryBean} for XML bean definition files.
064 *
065 * <p><b>Requires Hibernate 4.0 or higher.</b> As of Spring 4.0, it is compatible with
066 * (the quite refactored) Hibernate 4.3 as well. We recommend using the latest
067 * Hibernate 4.2.x or 4.3.x version, depending on whether you need to remain JPA 2.0
068 * compatible at runtime (Hibernate 4.2) or can upgrade to JPA 2.1 (Hibernate 4.3).
069 *
070 * <p><b>NOTE:</b> To set up Hibernate 4 for Spring-driven JTA transactions, make
071 * sure to either use the {@link #setJtaTransactionManager} method or to set the
072 * "hibernate.transaction.factory_class" property to {@link CMTTransactionFactory}.
073 * Otherwise, Hibernate's smart flushing mechanism won't work properly.
074 *
075 * @author Juergen Hoeller
076 * @since 3.1
077 * @see LocalSessionFactoryBean
078 */
079@SuppressWarnings("serial")
080public class LocalSessionFactoryBuilder extends Configuration {
081
082        private static final String RESOURCE_PATTERN = "/**/*.class";
083
084        private static final String PACKAGE_INFO_SUFFIX = ".package-info";
085
086        private static final TypeFilter[] DEFAULT_ENTITY_TYPE_FILTERS = new TypeFilter[] {
087                        new AnnotationTypeFilter(Entity.class, false),
088                        new AnnotationTypeFilter(Embeddable.class, false),
089                        new AnnotationTypeFilter(MappedSuperclass.class, false)};
090
091
092        private static TypeFilter converterTypeFilter;
093
094        static {
095                try {
096                        @SuppressWarnings("unchecked")
097                        Class<? extends Annotation> converterAnnotation = (Class<? extends Annotation>)
098                                        ClassUtils.forName("javax.persistence.Converter", LocalSessionFactoryBuilder.class.getClassLoader());
099                        converterTypeFilter = new AnnotationTypeFilter(converterAnnotation, false);
100                }
101                catch (ClassNotFoundException ex) {
102                        // JPA 2.1 API not available - Hibernate <4.3
103                }
104        }
105
106
107        private final ResourcePatternResolver resourcePatternResolver;
108
109        private RegionFactory cacheRegionFactory;
110
111        private TypeFilter[] entityTypeFilters = DEFAULT_ENTITY_TYPE_FILTERS;
112
113
114        /**
115         * Create a new LocalSessionFactoryBuilder for the given DataSource.
116         * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using
117         * (may be {@code null})
118         */
119        public LocalSessionFactoryBuilder(DataSource dataSource) {
120                this(dataSource, new PathMatchingResourcePatternResolver());
121        }
122
123        /**
124         * Create a new LocalSessionFactoryBuilder for the given DataSource.
125         * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using
126         * (may be {@code null})
127         * @param classLoader the ClassLoader to load application classes from
128         */
129        public LocalSessionFactoryBuilder(DataSource dataSource, ClassLoader classLoader) {
130                this(dataSource, new PathMatchingResourcePatternResolver(classLoader));
131        }
132
133        /**
134         * Create a new LocalSessionFactoryBuilder for the given DataSource.
135         * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using
136         * (may be {@code null})
137         * @param resourceLoader the ResourceLoader to load application classes from
138         */
139        @SuppressWarnings("deprecation")  // to be able to build against Hibernate 4.3
140        public LocalSessionFactoryBuilder(DataSource dataSource, ResourceLoader resourceLoader) {
141                getProperties().put(Environment.CURRENT_SESSION_CONTEXT_CLASS, SpringSessionContext.class.getName());
142                if (dataSource != null) {
143                        getProperties().put(Environment.DATASOURCE, dataSource);
144                }
145                // APP_CLASSLOADER is deprecated as of Hibernate 4.3 but we need to remain compatible with 4.0+
146                getProperties().put(AvailableSettings.APP_CLASSLOADER, resourceLoader.getClassLoader());
147                this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
148        }
149
150
151        /**
152         * Set the Spring {@link JtaTransactionManager} or the JTA {@link TransactionManager}
153         * to be used with Hibernate, if any. Allows for using a Spring-managed transaction
154         * manager for Hibernate 4's session and cache synchronization, with the
155         * "hibernate.transaction.jta.platform" automatically set to it. Also sets
156         * "hibernate.transaction.factory_class" to {@link CMTTransactionFactory},
157         * instructing Hibernate to interact with externally managed transactions.
158         * <p>A passed-in Spring {@link JtaTransactionManager} needs to contain a JTA
159         * {@link TransactionManager} reference to be usable here, except for the WebSphere
160         * case where we'll automatically set {@code WebSphereExtendedJtaPlatform} accordingly.
161         * <p>Note: If this is set, the Hibernate settings should not contain a JTA platform
162         * setting to avoid meaningless double configuration.
163         */
164        public LocalSessionFactoryBuilder setJtaTransactionManager(Object jtaTransactionManager) {
165                Assert.notNull(jtaTransactionManager, "Transaction manager reference must not be null");
166                if (jtaTransactionManager instanceof JtaTransactionManager) {
167                        boolean webspherePresent = ClassUtils.isPresent("com.ibm.wsspi.uow.UOWManager", getClass().getClassLoader());
168                        if (webspherePresent) {
169                                getProperties().put(AvailableSettings.JTA_PLATFORM,
170                                                ConfigurableJtaPlatform.getJtaPlatformBasePackage() + "internal.WebSphereExtendedJtaPlatform");
171                        }
172                        else {
173                                JtaTransactionManager jtaTm = (JtaTransactionManager) jtaTransactionManager;
174                                if (jtaTm.getTransactionManager() == null) {
175                                        throw new IllegalArgumentException(
176                                                        "Can only apply JtaTransactionManager which has a TransactionManager reference set");
177                                }
178                                getProperties().put(AvailableSettings.JTA_PLATFORM,
179                                                new ConfigurableJtaPlatform(jtaTm.getTransactionManager(), jtaTm.getUserTransaction(),
180                                                                jtaTm.getTransactionSynchronizationRegistry()).getJtaPlatformProxy());
181                        }
182                }
183                else if (jtaTransactionManager instanceof TransactionManager) {
184                        getProperties().put(AvailableSettings.JTA_PLATFORM,
185                                        new ConfigurableJtaPlatform((TransactionManager) jtaTransactionManager, null, null).getJtaPlatformProxy());
186                }
187                else {
188                        throw new IllegalArgumentException(
189                                        "Unknown transaction manager type: " + jtaTransactionManager.getClass().getName());
190                }
191                getProperties().put(AvailableSettings.TRANSACTION_STRATEGY, new CMTTransactionFactory());
192                return this;
193        }
194
195        /**
196         * Set a Hibernate 4.1/4.2/4.3 {@code MultiTenantConnectionProvider} to be passed
197         * on to the SessionFactory: as an instance, a Class, or a String class name.
198         * <p>Note that the package location of the {@code MultiTenantConnectionProvider}
199         * interface changed between Hibernate 4.2 and 4.3. This method accepts both variants.
200         * @since 4.0
201         * @see AvailableSettings#MULTI_TENANT_CONNECTION_PROVIDER
202         */
203        public LocalSessionFactoryBuilder setMultiTenantConnectionProvider(Object multiTenantConnectionProvider) {
204                getProperties().put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
205                return this;
206        }
207
208        /**
209         * Set a Hibernate 4.1/4.2/4.3 {@code CurrentTenantIdentifierResolver} to be passed
210         * on to the SessionFactory: as an instance, a Class, or a String class name.
211         * @since 4.0
212         * @see AvailableSettings#MULTI_TENANT_IDENTIFIER_RESOLVER
213         */
214        public LocalSessionFactoryBuilder setCurrentTenantIdentifierResolver(Object currentTenantIdentifierResolver) {
215                getProperties().put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);
216                return this;
217        }
218
219        /**
220         * Set the Hibernate RegionFactory to use for the SessionFactory.
221         * Allows for using a Spring-managed RegionFactory instance.
222         * <p>Note: If this is set, the Hibernate settings should not define a
223         * cache provider to avoid meaningless double configuration.
224         * @since 4.0
225         * @see org.hibernate.cache.spi.RegionFactory
226         */
227        public LocalSessionFactoryBuilder setCacheRegionFactory(RegionFactory cacheRegionFactory) {
228                this.cacheRegionFactory = cacheRegionFactory;
229                return this;
230        }
231
232        /**
233         * Specify custom type filters for Spring-based scanning for entity classes.
234         * <p>Default is to search all specified packages for classes annotated with
235         * {@code @javax.persistence.Entity}, {@code @javax.persistence.Embeddable}
236         * or {@code @javax.persistence.MappedSuperclass}.
237         * @since 4.1
238         * @see #scanPackages
239         */
240        public LocalSessionFactoryBuilder setEntityTypeFilters(TypeFilter... entityTypeFilters) {
241                this.entityTypeFilters = entityTypeFilters;
242                return this;
243        }
244
245        /**
246         * Add the given annotated classes in a batch.
247         * @see #addAnnotatedClass
248         * @see #scanPackages
249         */
250        public LocalSessionFactoryBuilder addAnnotatedClasses(Class<?>... annotatedClasses) {
251                for (Class<?> annotatedClass : annotatedClasses) {
252                        addAnnotatedClass(annotatedClass);
253                }
254                return this;
255        }
256
257        /**
258         * Add the given annotated packages in a batch.
259         * @see #addPackage
260         * @see #scanPackages
261         */
262        public LocalSessionFactoryBuilder addPackages(String... annotatedPackages) {
263                for (String annotatedPackage : annotatedPackages) {
264                        addPackage(annotatedPackage);
265                }
266                return this;
267        }
268
269        /**
270         * Perform Spring-based scanning for entity classes, registering them
271         * as annotated classes with this {@code Configuration}.
272         * @param packagesToScan one or more Java package names
273         * @throws HibernateException if scanning fails for any reason
274         */
275        public LocalSessionFactoryBuilder scanPackages(String... packagesToScan) throws HibernateException {
276                Set<String> entityClassNames = new TreeSet<String>();
277                Set<String> converterClassNames = new TreeSet<String>();
278                Set<String> packageNames = new TreeSet<String>();
279                try {
280                        for (String pkg : packagesToScan) {
281                                String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
282                                                ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN;
283                                Resource[] resources = this.resourcePatternResolver.getResources(pattern);
284                                MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
285                                for (Resource resource : resources) {
286                                        if (resource.isReadable()) {
287                                                MetadataReader reader = readerFactory.getMetadataReader(resource);
288                                                String className = reader.getClassMetadata().getClassName();
289                                                if (matchesEntityTypeFilter(reader, readerFactory)) {
290                                                        entityClassNames.add(className);
291                                                }
292                                                else if (converterTypeFilter != null && converterTypeFilter.match(reader, readerFactory)) {
293                                                        converterClassNames.add(className);
294                                                }
295                                                else if (className.endsWith(PACKAGE_INFO_SUFFIX)) {
296                                                        packageNames.add(className.substring(0, className.length() - PACKAGE_INFO_SUFFIX.length()));
297                                                }
298                                        }
299                                }
300                        }
301                }
302                catch (IOException ex) {
303                        throw new MappingException("Failed to scan classpath for unlisted classes", ex);
304                }
305                try {
306                        ClassLoader cl = this.resourcePatternResolver.getClassLoader();
307                        for (String className : entityClassNames) {
308                                addAnnotatedClass(cl.loadClass(className));
309                        }
310                        for (String className : converterClassNames) {
311                                ConverterRegistrationDelegate.registerConverter(this, cl.loadClass(className));
312                        }
313                        for (String packageName : packageNames) {
314                                addPackage(packageName);
315                        }
316                }
317                catch (ClassNotFoundException ex) {
318                        throw new MappingException("Failed to load annotated classes from classpath", ex);
319                }
320                return this;
321        }
322
323        /**
324         * Check whether any of the configured entity type filters matches
325         * the current class descriptor contained in the metadata reader.
326         */
327        private boolean matchesEntityTypeFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {
328                if (this.entityTypeFilters != null) {
329                        for (TypeFilter filter : this.entityTypeFilters) {
330                                if (filter.match(reader, readerFactory)) {
331                                        return true;
332                                }
333                        }
334                }
335                return false;
336        }
337
338
339        // Overridden methods from Hibernate's Configuration class
340
341        @Override
342        public Settings buildSettings(Properties props, ServiceRegistry serviceRegistry) throws HibernateException {
343                Settings settings = super.buildSettings(props, serviceRegistry);
344                if (this.cacheRegionFactory != null) {
345                        try {
346                                Method setRegionFactory = Settings.class.getDeclaredMethod("setRegionFactory", RegionFactory.class);
347                                setRegionFactory.setAccessible(true);
348                                setRegionFactory.invoke(settings, this.cacheRegionFactory);
349                        }
350                        catch (Exception ex) {
351                                throw new IllegalStateException("Failed to invoke Hibernate's setRegionFactory method", ex);
352                        }
353                }
354                return settings;
355        }
356
357        /**
358         * Build the {@code SessionFactory}.
359         */
360        @Override
361        @SuppressWarnings("deprecation")
362        public SessionFactory buildSessionFactory() throws HibernateException {
363                ClassLoader appClassLoader = (ClassLoader) getProperties().get(AvailableSettings.APP_CLASSLOADER);
364                Thread currentThread = Thread.currentThread();
365                ClassLoader threadContextClassLoader = currentThread.getContextClassLoader();
366                boolean overrideClassLoader =
367                                (appClassLoader != null && !appClassLoader.equals(threadContextClassLoader));
368                if (overrideClassLoader) {
369                        currentThread.setContextClassLoader(appClassLoader);
370                }
371                try {
372                        return super.buildSessionFactory();
373                }
374                finally {
375                        if (overrideClassLoader) {
376                                currentThread.setContextClassLoader(threadContextClassLoader);
377                        }
378                }
379        }
380
381
382        /**
383         * Inner class to avoid hard dependency on JPA 2.1 / Hibernate 4.3.
384         */
385        private static class ConverterRegistrationDelegate {
386
387                @SuppressWarnings("unchecked")
388                public static void registerConverter(Configuration config, Class<?> converterClass) {
389                        config.addAttributeConverter((Class<? extends AttributeConverter<?, ?>>) converterClass);
390                }
391        }
392
393}