001/*
002 * (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Stephane Lacoin
016 *     Florent Guillaume
017 */
018package org.nuxeo.ecm.core.persistence;
019
020import java.io.IOException;
021import java.net.URL;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Properties;
028
029import javax.naming.NamingException;
030import javax.persistence.EntityManagerFactory;
031import javax.persistence.spi.PersistenceUnitTransactionType;
032import javax.transaction.Synchronization;
033import javax.transaction.SystemException;
034import javax.transaction.Transaction;
035import javax.transaction.TransactionManager;
036
037import org.hibernate.ConnectionReleaseMode;
038import org.hibernate.HibernateException;
039import org.hibernate.cfg.Environment;
040import org.hibernate.ejb.Ejb3Configuration;
041import org.hibernate.ejb.HibernatePersistence;
042import org.hibernate.ejb.transaction.JoinableCMTTransaction;
043import org.hibernate.ejb.transaction.JoinableCMTTransactionFactory;
044import org.hibernate.jdbc.JDBCContext;
045import org.hibernate.transaction.JDBCTransactionFactory;
046import org.hibernate.transaction.TransactionFactory;
047import org.hibernate.transaction.TransactionManagerLookup;
048import org.nuxeo.common.xmap.XMap;
049import org.nuxeo.common.xmap.annotation.XNode;
050import org.nuxeo.common.xmap.annotation.XNodeList;
051import org.nuxeo.common.xmap.annotation.XNodeMap;
052import org.nuxeo.common.xmap.annotation.XObject;
053import org.nuxeo.runtime.api.Framework;
054import org.nuxeo.runtime.datasource.ConnectionHelper;
055import org.nuxeo.runtime.datasource.DataSourceHelper;
056import org.nuxeo.runtime.jtajca.NamingContextFactory;
057import org.nuxeo.runtime.jtajca.NuxeoContainer;
058import org.nuxeo.runtime.transaction.TransactionHelper;
059
060/**
061 */
062@XObject("hibernateConfiguration")
063public class HibernateConfiguration implements EntityManagerFactoryProvider {
064
065    public static final String RESOURCE_LOCAL = PersistenceUnitTransactionType.RESOURCE_LOCAL.name();
066
067    public static final String JTA = PersistenceUnitTransactionType.JTA.name();
068
069    public static final String TXTYPE_PROPERTY_NAME = "org.nuxeo.runtime.txType";
070
071    @XNode("@name")
072    public String name;
073
074    @XNode("datasource")
075    public void setDatasource(String name) {
076        String expandedValue = Framework.expandVars(name);
077        if (expandedValue.startsWith("$")) {
078            throw new PersistenceError("Cannot expand " + name + " for datasource");
079        }
080        hibernateProperties.put("hibernate.connection.datasource", DataSourceHelper.getDataSourceJNDIName(name));
081    }
082
083    @XNodeMap(value = "properties/property", key = "@name", type = Properties.class, componentType = String.class)
084    public final Properties hibernateProperties = new Properties();
085
086    @XNodeList(value = "classes/class", type = ArrayList.class, componentType = Class.class)
087    public final List<Class<?>> annotedClasses = new ArrayList<Class<?>>();
088
089    public void addAnnotedClass(Class<?> annotedClass) {
090        annotedClasses.add(annotedClass);
091    }
092
093    public void removeAnnotedClass(Class<?> annotedClass) {
094        annotedClasses.remove(annotedClass);
095    }
096
097    protected Ejb3Configuration cfg;
098
099    public Ejb3Configuration setupConfiguration() {
100        return setupConfiguration(null);
101    }
102
103    public Ejb3Configuration setupConfiguration(Map<String, String> properties) {
104        cfg = new Ejb3Configuration();
105
106        if (properties != null) {
107            cfg.configure(name, properties);
108        } else {
109            cfg.configure(name, Collections.emptyMap());
110        }
111
112        // Load hibernate properties
113        cfg.addProperties(hibernateProperties);
114
115        // Add annnoted classes if any
116        for (Class<?> annotedClass : annotedClasses) {
117            cfg.addAnnotatedClass(annotedClass);
118        }
119
120        return cfg;
121    }
122
123    @Override
124    public EntityManagerFactory getFactory(String txType) {
125        Map<String, String> properties = new HashMap<String, String>();
126        if (txType == null) {
127            txType = getTxType();
128        }
129        properties.put(HibernatePersistence.TRANSACTION_TYPE, txType);
130        if (txType.equals(JTA)) {
131            Class<?> transactionFactoryClass;
132            if (ConnectionHelper.useSingleConnection(null)) {
133                transactionFactoryClass = NuxeoTransactionFactory.class;
134            } else {
135                transactionFactoryClass = JoinableCMTTransactionFactory.class;
136            }
137            properties.put(Environment.TRANSACTION_STRATEGY, transactionFactoryClass.getName());
138            properties.put(Environment.TRANSACTION_MANAGER_STRATEGY, NuxeoTransactionManagerLookup.class.getName());
139        } else if (txType.equals(RESOURCE_LOCAL)) {
140            properties.put(Environment.TRANSACTION_STRATEGY, JDBCTransactionFactory.class.getName());
141        }
142        if (cfg == null) {
143            setupConfiguration(properties);
144        }
145        Properties props = cfg.getProperties();
146        if (props.get(Environment.URL) == null) {
147            // don't set up our connection provider for unit tests
148            // that use an explicit driver + connection URL and so use
149            // a DriverManagerConnectionProvider
150            props.put(Environment.CONNECTION_PROVIDER, NuxeoConnectionProvider.class.getName());
151        }
152        if (txType.equals(RESOURCE_LOCAL)) {
153            props.remove(Environment.DATASOURCE);
154        } else {
155            String dsname = props.getProperty(Environment.DATASOURCE);
156            dsname = DataSourceHelper.getDataSourceJNDIName(dsname);
157            props.put(Environment.DATASOURCE, dsname);
158            props.put(Environment.JNDI_CLASS, NamingContextFactory.class.getName());
159            props.put(Environment.JNDI_PREFIX.concat(".").concat(javax.naming.Context.URL_PKG_PREFIXES),
160                    NuxeoContainer.class.getPackage().getName());
161        }
162        return createEntityManagerFactory(properties);
163    }
164
165    /**
166     * Don't close the connection aggressively after each statement.
167     */
168    // needs to be public as hibernate calls newInstance
169    public static class NuxeoTransactionFactory extends JoinableCMTTransactionFactory {
170        @Override
171        public ConnectionReleaseMode getDefaultReleaseMode() {
172            return ConnectionReleaseMode.AFTER_TRANSACTION;
173        }
174
175        @Override
176        public org.hibernate.Transaction createTransaction(JDBCContext jdbcContext,
177                TransactionFactory.Context transactionContext) throws HibernateException {
178            return new NuxeoHibernateTransaction(jdbcContext, transactionContext);
179        }
180    }
181
182    /**
183     * Hibernate transaction that will register a synchronization that runs before the one from ConnectionHelper in
184     * single-datasource mode.
185     * <p>
186     * Needed because the sync from org.hibernate.ejb.EntityManagerImpl#close must run before the one from
187     * ConnectionHelper.
188     */
189    public static class NuxeoHibernateTransaction extends JoinableCMTTransaction {
190        public NuxeoHibernateTransaction(JDBCContext jdbcContext, TransactionFactory.Context transactionContext) {
191            super(jdbcContext, transactionContext);
192        }
193
194        @Override
195        public void registerSynchronization(Synchronization sync) throws HibernateException {
196            boolean registered;
197            try {
198                registered = ConnectionHelper.registerSynchronization(sync);
199            } catch (SystemException e) {
200                throw new HibernateException(e);
201            }
202            if (!registered) {
203                super.registerSynchronization(sync);
204            }
205        }
206    }
207
208    // this must be executed always outside a transaction
209    // because SchemaUpdate tries to setAutoCommit(true)
210    // so we use a new thread
211    protected EntityManagerFactory createEntityManagerFactory(final Map<String, String> properties) {
212        final EntityManagerFactory[] emf = new EntityManagerFactory[1];
213        Thread t = new Thread("persistence-init-" + name) {
214            @SuppressWarnings("deprecation")
215            @Override
216            public void run() {
217                emf[0] = cfg.createEntityManagerFactory(properties);
218            };
219        };
220        try {
221            t.start();
222            t.join();
223        } catch (InterruptedException e) {
224            throw new RuntimeException(e);
225        }
226        return emf[0];
227    }
228
229    /**
230     * Hibernate Transaction Manager Lookup that uses our framework.
231     */
232    public static class NuxeoTransactionManagerLookup implements TransactionManagerLookup {
233        public NuxeoTransactionManagerLookup() {
234            // look up UserTransaction once to know its JNDI name
235            try {
236                TransactionHelper.lookupUserTransaction();
237            } catch (NamingException e) {
238                // ignore
239            }
240        }
241
242        @Override
243        public TransactionManager getTransactionManager(Properties props) {
244            try {
245                return TransactionHelper.lookupTransactionManager();
246            } catch (NamingException e) {
247                throw new HibernateException(e.getMessage(), e);
248            }
249        }
250
251        @Override
252        public String getUserTransactionName() {
253            return TransactionHelper.getUserTransactionJNDIName();
254        }
255
256        @Override
257        public Object getTransactionIdentifier(Transaction transaction) {
258            return transaction;
259        }
260    }
261
262    @Override
263    public EntityManagerFactory getFactory() {
264        return getFactory(null);
265    }
266
267    public static String getTxType() {
268        String txType;
269        if (Framework.isInitialized()) {
270            txType = Framework.getProperty(TXTYPE_PROPERTY_NAME);
271            if (txType == null) {
272                try {
273                    TransactionHelper.lookupTransactionManager();
274                    txType = JTA;
275                } catch (NamingException e) {
276                    txType = RESOURCE_LOCAL;
277                }
278            }
279        } else {
280            txType = RESOURCE_LOCAL;
281        }
282        return txType;
283    }
284
285    public static HibernateConfiguration load(URL location) {
286        XMap map = new XMap();
287        map.register(HibernateConfiguration.class);
288        try {
289            return (HibernateConfiguration) map.load(location);
290        } catch (IOException e) {
291            throw new PersistenceError("Cannot load hibernate configuration from " + location, e);
292        }
293    }
294
295    public void merge(HibernateConfiguration other) {
296        assert name.equals(other.name) : " cannot merge configuration that do not have the same persistence unit";
297        annotedClasses.addAll(other.annotedClasses);
298        hibernateProperties.clear();
299        hibernateProperties.putAll(other.hibernateProperties);
300    }
301
302}