001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019package org.apache.isis.persistence.jdo.spring.integration;
020
021import java.io.IOException;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.Properties;
025import javax.jdo.JDOException;
026import javax.jdo.JDOHelper;
027import javax.jdo.PersistenceManagerFactory;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031
032import org.springframework.beans.factory.BeanClassLoaderAware;
033import org.springframework.beans.factory.DisposableBean;
034import org.springframework.beans.factory.FactoryBean;
035import org.springframework.beans.factory.InitializingBean;
036import org.springframework.core.io.Resource;
037import org.springframework.core.io.support.PropertiesLoaderUtils;
038import org.springframework.dao.DataAccessException;
039import org.springframework.dao.support.PersistenceExceptionTranslator;
040import org.springframework.util.CollectionUtils;
041
042/**
043 * {@link org.springframework.beans.factory.FactoryBean} that creates a
044 * JDO {@link javax.jdo.PersistenceManagerFactory}. This is the usual way to
045 * set up a shared JDO PersistenceManagerFactory in a Spring application context;
046 * the PersistenceManagerFactory can then be passed to JDO-based DAOs via
047 * dependency injection. Note that switching to a JNDI lookup or to a bean-style
048 * PersistenceManagerFactory instance is just a matter of configuration!
049 *
050 * <p><b>NOTE: This class requires JDO 3.0 or higher, as of Spring 4.0.</b>
051 * It will also expose the JPA {@link javax.persistence.EntityManagerFactory} as long
052 * as the JDO provider creates a {@link javax.jdo.JDOEntityManagerFactory} reference
053 * underneath, which means that this class can be used as a replacement for
054 * {@link org.springframework.orm.jpa.LocalEntityManagerFactoryBean} in such a scenario.
055 *
056 * <p>Configuration settings can either be read from a properties file,
057 * specified as "configLocation", or locally specified. Properties
058 * specified as "jdoProperties" here will override any settings in a file.
059 * You may alternatively specify a "persistenceManagerFactoryName",
060 * referring to a PMF definition in "META-INF/jdoconfig.xml"
061 * (see {@link #setPersistenceManagerFactoryName}).
062 *
063 * <p>This class also implements the
064 * {@link org.springframework.dao.support.PersistenceExceptionTranslator}
065 * interface, as autodetected by Spring's
066 * {@link org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor},
067 * for AOP-based translation of native exceptions to Spring DataAccessExceptions.
068 * Hence, the presence of a LocalPersistenceManagerFactoryBean automatically enables
069 * a PersistenceExceptionTranslationPostProcessor to translate JDO exceptions.
070 *
071 * <p><b>Alternative: Configuration of a PersistenceManagerFactory provider bean</b>
072 *
073 * <p>As alternative to the properties-driven approach that this FactoryBean offers
074 * (which is analogous to using the standard JDOHelper class with a Properties
075 * object that is populated with standard JDO properties), you can set up an
076 * instance of your PersistenceManagerFactory implementation class directly.
077 *
078 * <p>Like a DataSource, a PersistenceManagerFactory is encouraged to
079 * support bean-style configuration, which makes it very easy to set up as
080 * Spring-managed bean. The implementation class becomes the bean class;
081 * the remaining properties are applied as bean properties (starting with
082 * lower-case characters, in contrast to the corresponding JDO properties).
083 *
084 * <p>For example, in case of <a href="http://www.jpox.org">JPOX</a>:
085 *
086 * <p><pre class="code">
087 * &lt;bean id="persistenceManagerFactory" class="org.jpox.PersistenceManagerFactoryImpl" destroy-method="close"&gt;
088 *   &lt;property name="connectionFactory" ref="dataSource"/&gt;
089 *   &lt;property name="nontransactionalRead" value="true"/&gt;
090 * &lt;/bean&gt;
091 * </pre>
092 *
093 * <p>Note that such direct setup of a PersistenceManagerFactory implementation
094 * is the only way to pass an external connection factory (i.e. a JDBC DataSource)
095 * into a JDO PersistenceManagerFactory. With the standard properties-driven approach,
096 * you can only use an internal connection pool or a JNDI DataSource.
097 *
098 * <p>The {@code close()} method is standardized in JDO; don't forget to
099 * specify it as "destroy-method" for any PersistenceManagerFactory instance.
100 * Note that this FactoryBean will automatically invoke {@code close()} for
101 * the PersistenceManagerFactory that it creates, without any special configuration.
102 *
103 * @see JdoTransactionManager#setPersistenceManagerFactory
104 * @see org.springframework.jndi.JndiObjectFactoryBean
105 * @see javax.jdo.JDOHelper#getPersistenceManagerFactory
106 * @see javax.jdo.PersistenceManagerFactory#setConnectionFactory
107 * @see javax.jdo.PersistenceManagerFactory#close()
108 * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor
109 */
110public class LocalPersistenceManagerFactoryBean implements FactoryBean<PersistenceManagerFactory>,
111                BeanClassLoaderAware, InitializingBean, DisposableBean, PersistenceExceptionTranslator {
112
113        protected final Log logger = LogFactory.getLog(getClass());
114
115        private String persistenceManagerFactoryName;
116
117        private Resource configLocation;
118
119        private final Map<String, Object> jdoPropertyMap = new HashMap<String, Object>();
120
121        protected ClassLoader beanClassLoader;
122
123        private PersistenceManagerFactory persistenceManagerFactory;
124
125        private JdoDialect jdoDialect;
126
127
128        /**
129         * Specify the name of the desired PersistenceManagerFactory.
130         * <p>This may either be a properties resource in the classpath if such a resource
131         * exists, or a PMF definition with that name from "META-INF/jdoconfig.xml",
132         * or a JPA EntityManagerFactory cast to a PersistenceManagerFactory based on the
133         * persistence-unit name from "META-INF/persistence.xml" (JPA).
134         * <p>Default is none: Either 'persistenceManagerFactoryName' or 'configLocation'
135         * or 'jdoProperties' needs to be specified.
136         * @see #setConfigLocation
137         * @see #setJdoProperties
138         */
139        public void setPersistenceManagerFactoryName(String persistenceManagerFactoryName) {
140                this.persistenceManagerFactoryName = persistenceManagerFactoryName;
141        }
142
143        /**
144         * Set the location of the JDO properties config file, for example
145         * as classpath resource "classpath:kodo.properties".
146         * <p>Note: Can be omitted when all necessary properties are
147         * specified locally via this bean.
148         */
149        public void setConfigLocation(Resource configLocation) {
150                this.configLocation = configLocation;
151        }
152
153        /**
154         * Set JDO properties, such as"javax.jdo.PersistenceManagerFactoryClass".
155         * <p>Can be used to override values in a JDO properties config file,
156         * or to specify all necessary properties locally.
157         * <p>Can be populated with a String "value" (parsed via PropertiesEditor)
158         * or a "props" element in XML bean definitions.
159         */
160        public void setJdoProperties(Properties jdoProperties) {
161                CollectionUtils.mergePropertiesIntoMap(jdoProperties, this.jdoPropertyMap);
162        }
163
164        /**
165         * Specify JDO properties as a Map, to be passed into
166         * {@code JDOHelper.getPersistenceManagerFactory} (if any).
167         * <p>Can be populated with a "map" or "props" element in XML bean definitions.
168         * @see javax.jdo.JDOHelper#getPersistenceManagerFactory(java.util.Map)
169         */
170        public void setJdoPropertyMap(Map<String, Object> jdoProperties) {
171                if (jdoProperties != null) {
172                        this.jdoPropertyMap.putAll(jdoProperties);
173                }
174        }
175
176        /**
177         * Allow Map access to the JDO properties to be passed to the JDOHelper,
178         * with the option to add or override specific entries.
179         * <p>Useful for specifying entries directly, for example via
180         * "jdoPropertyMap[myKey]".
181         */
182        public Map<String, Object> getJdoPropertyMap() {
183                return this.jdoPropertyMap;
184        }
185        /**
186         * Set the JDO dialect to use for the PersistenceExceptionTranslator
187         * functionality of this factory.
188         * <p>Default is a DefaultJdoDialect based on the PersistenceManagerFactory's
189         * underlying DataSource, if any.
190         * @see JdoDialect#translateException
191         * @see #translateExceptionIfPossible
192         * @see org.springframework.dao.support.PersistenceExceptionTranslator
193         */
194        public void setJdoDialect(JdoDialect jdoDialect) {
195                this.jdoDialect = jdoDialect;
196        }
197
198        @Override
199        public void setBeanClassLoader(ClassLoader beanClassLoader) {
200                this.beanClassLoader = beanClassLoader;
201        }
202
203
204        /**
205         * Initialize the PersistenceManagerFactory for the given location.
206         * @throws IllegalArgumentException in case of illegal property values
207         * @throws IOException if the properties could not be loaded from the given location
208         * @throws JDOException in case of JDO initialization errors
209         */
210        @Override
211        public void afterPropertiesSet() throws IllegalArgumentException, IOException, JDOException {
212                if (this.persistenceManagerFactoryName != null) {
213                        if (this.configLocation != null || !this.jdoPropertyMap.isEmpty()) {
214                                throw new IllegalStateException("'configLocation'/'jdoProperties' not supported in " +
215                                                "combination with 'persistenceManagerFactoryName' - specify one or the other, not both");
216                        }
217                        if (logger.isInfoEnabled()) {
218                                logger.info("Building new JDO PersistenceManagerFactory for name '" +
219                                                this.persistenceManagerFactoryName + "'");
220                        }
221                        this.persistenceManagerFactory = newPersistenceManagerFactory(this.persistenceManagerFactoryName);
222                }
223
224                else {
225                        Map<String, Object> mergedProps = new HashMap<String, Object>();
226                        if (this.configLocation != null) {
227                                if (logger.isInfoEnabled()) {
228                                        logger.info("Loading JDO config from [" + this.configLocation + "]");
229                                }
230                                CollectionUtils.mergePropertiesIntoMap(
231                                                PropertiesLoaderUtils.loadProperties(this.configLocation), mergedProps);
232                        }
233                        mergedProps.putAll(this.jdoPropertyMap);
234                        logger.info("Building new JDO PersistenceManagerFactory");
235                        this.persistenceManagerFactory = newPersistenceManagerFactory(mergedProps);
236                }
237
238                // Build default JdoDialect if none explicitly specified.
239                if (this.jdoDialect == null) {
240                        this.jdoDialect = new DefaultJdoDialect(this.persistenceManagerFactory.getConnectionFactory());
241                }
242        }
243
244        /**
245         * Subclasses can override this to perform custom initialization of the
246         * PersistenceManagerFactory instance, creating it for the specified name.
247         * <p>The default implementation invokes JDOHelper's
248         * {@code getPersistenceManagerFactory(String)} method.
249         * A custom implementation could prepare the instance in a specific way,
250         * or use a custom PersistenceManagerFactory implementation.
251         * @param name the name of the desired PersistenceManagerFactory
252         * @return the PersistenceManagerFactory instance
253         * @see javax.jdo.JDOHelper#getPersistenceManagerFactory(String)
254         */
255        protected PersistenceManagerFactory newPersistenceManagerFactory(String name) {
256                return JDOHelper.getPersistenceManagerFactory(name, this.beanClassLoader);
257        }
258
259        /**
260         * Subclasses can override this to perform custom initialization of the
261         * PersistenceManagerFactory instance, creating it via the given Properties
262         * that got prepared by this LocalPersistenceManagerFactoryBean.
263         * <p>The default implementation invokes JDOHelper's
264         * {@code getPersistenceManagerFactory(Map)} method.
265         * A custom implementation could prepare the instance in a specific way,
266         * or use a custom PersistenceManagerFactory implementation.
267         * @param props the merged properties prepared by this LocalPersistenceManagerFactoryBean
268         * @return the PersistenceManagerFactory instance
269         * @see javax.jdo.JDOHelper#getPersistenceManagerFactory(java.util.Map)
270         */
271        protected PersistenceManagerFactory newPersistenceManagerFactory(Map<?, ?> props) {
272                return JDOHelper.getPersistenceManagerFactory(props, this.beanClassLoader);
273        }
274
275
276        /**
277         * Return the singleton PersistenceManagerFactory.
278         */
279        @Override
280        public PersistenceManagerFactory getObject() {
281                return this.persistenceManagerFactory;
282        }
283
284        @Override
285        public Class<? extends PersistenceManagerFactory> getObjectType() {
286                return (this.persistenceManagerFactory != null ?
287                        this.persistenceManagerFactory.getClass() : PersistenceManagerFactory.class);
288        }
289
290        @Override
291        public boolean isSingleton() {
292                return true;
293        }
294
295
296        /**
297         * Implementation of the PersistenceExceptionTranslator interface,
298         * as autodetected by Spring's PersistenceExceptionTranslationPostProcessor.
299         * <p>Converts the exception if it is a JDOException, preferably using a specified
300         * JdoDialect. Else returns {@code null} to indicate an unknown exception.
301         * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor
302         * @see JdoDialect#translateException
303         * @see PersistenceManagerFactoryUtils#convertJdoAccessException
304         */
305        @Override
306        public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
307                if (ex instanceof JDOException) {
308                        if (this.jdoDialect != null) {
309                                return this.jdoDialect.translateException((JDOException) ex);
310                        }
311                        else {
312                                return PersistenceManagerFactoryUtils.convertJdoAccessException((JDOException) ex);
313                        }
314                }
315                return null;
316        }
317
318
319        /**
320         * Close the PersistenceManagerFactory on bean factory shutdown.
321         */
322        @Override
323        public void destroy() {
324                logger.info("Closing JDO PersistenceManagerFactory");
325                this.persistenceManagerFactory.close();
326        }
327
328}