001    /*
002     * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org
003     * Copyright (C) 2011 NightLabs Consulting GmbH
004     *
005     * This program is free software: you can redistribute it and/or modify
006     * it under the terms of the GNU Affero General Public License as
007     * published by the Free Software Foundation, either version 3 of the
008     * License, or (at your option) any later version.
009     *
010     * This program is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU Affero General Public License for more details.
014     *
015     * You should have received a copy of the GNU Affero General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    package org.cumulus4j.store;
019    
020    import java.util.HashMap;
021    import java.util.Locale;
022    import java.util.Map;
023    import java.util.Set;
024    
025    import javax.jdo.JDOHelper;
026    import javax.jdo.PersistenceManager;
027    import javax.jdo.PersistenceManagerFactory;
028    import javax.transaction.xa.XAException;
029    import javax.transaction.xa.XAResource;
030    import javax.transaction.xa.Xid;
031    
032    import org.cumulus4j.store.model.ClassMeta;
033    import org.cumulus4j.store.model.DataEntry;
034    import org.cumulus4j.store.model.DatastoreVersion;
035    import org.cumulus4j.store.model.EmbeddedClassMeta;
036    import org.cumulus4j.store.model.EmbeddedFieldMeta;
037    import org.cumulus4j.store.model.EncryptionCoordinateSet;
038    import org.cumulus4j.store.model.FieldMeta;
039    import org.cumulus4j.store.model.IndexEntry;
040    import org.cumulus4j.store.model.IndexEntryContainerSize;
041    import org.cumulus4j.store.model.Sequence2;
042    import org.cumulus4j.store.resource.ResourceHelper;
043    import org.datanucleus.PersistenceConfiguration;
044    import org.datanucleus.store.StoreManager;
045    import org.datanucleus.store.connection.AbstractConnectionFactory;
046    import org.datanucleus.store.connection.AbstractManagedConnection;
047    import org.datanucleus.store.connection.ManagedConnection;
048    import org.datanucleus.util.NucleusLogger;
049    import org.datanucleus.util.StringUtils;
050    
051    /**
052     * <p>
053     * Connection factory implementation for Cumulus4j-connections.
054     * </p><p>
055     * A "connection" in Cumulus4J is a <code>PersistenceManager</code> for the backing datastore.
056     * When the transaction in Cumulus4J is committed, the equivalent transaction is committed in the PM(s) of the
057     * backing datastore(s).
058     * </p><p>
059     * How to configure a connection factory is documented on
060     * <a href="http://cumulus4j.org/1.1.1/documentation/persistence-api.html">Persistence API</a>.
061     * </p>
062     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
063     */
064    public class Cumulus4jConnectionFactory extends AbstractConnectionFactory
065    {
066            /** PMF for DataEntry, ClassMeta+FieldMeta, and optionally index data (if not using pmfIndex). */
067            private PersistenceManagerFactory pmf;
068    
069            /** Optional PMF for index data. */
070            private PersistenceManagerFactory pmfIndex;
071    
072            private String[] propertiesToForward = {
073                            "datanucleus.ConnectionDriverName",
074                            "datanucleus.ConnectionURL",
075                            "datanucleus.ConnectionUserName",
076                            "datanucleus.ConnectionFactory",
077                            "datanucleus.ConnectionFactoryName",
078                            "datanucleus.ConnectionFactory2",
079                            "datanucleus.ConnectionFactory2Name"
080            };
081    
082            private static final String CUMULUS4J_PROPERTY_PREFIX = "cumulus4j.";
083            private static final String CUMULUS4J_INDEX_PROPERTY_PREFIX = "cumulus4j.index.";
084    
085            private static final String[] CUMULUS4J_FORWARD_PROPERTY_PREFIXES = {
086                    CUMULUS4J_PROPERTY_PREFIX + "datanucleus.",
087                    CUMULUS4J_PROPERTY_PREFIX + "javax."
088            };
089    
090            private static final String[] CUMULUS4J_INDEX_FORWARD_PROPERTY_PREFIXES = {
091                    CUMULUS4J_INDEX_PROPERTY_PREFIX + "datanucleus.",
092                    CUMULUS4J_INDEX_PROPERTY_PREFIX + "javax."
093            };
094    
095            public Cumulus4jConnectionFactory(StoreManager storeMgr, String resourceType) {
096                    super(storeMgr, resourceType);
097    
098                    Map<String, Object> backendProperties = ResourceHelper.getCumulus4jBackendProperties();
099                    Map<String, Object> persistenceProperties = ResourceHelper.getCumulus4jPersistenceProperties();
100                    Map<String, Object> backendIndexProperties = null;
101    
102                    PersistenceConfiguration persistenceConfiguration = storeMgr.getNucleusContext().getPersistenceConfiguration();
103                    for (Map.Entry<String, Object> me : persistenceProperties.entrySet()) {
104                            if (me.getKey() == null) // can't happen, but better play safe
105                                    continue;
106    
107                            if (!persistenceConfiguration.hasProperty(me.getKey())) // we load our defaults after the user config, hence we must not override!
108                                    persistenceConfiguration.setProperty(me.getKey(), me.getValue());
109                    }
110    
111                    // Copy the properties that are directly (as is) forwarded.
112                    for (String propKey : propertiesToForward) {
113                            Object propValue = persistenceConfiguration.getProperty(propKey);
114                            if (propValue != null)
115                                    backendProperties.put(propKey.toLowerCase(Locale.ENGLISH), propValue);
116                    }
117    
118                    // Copy the properties that are prefixed with "cumulus4j." and thus forwarded.
119                    for (Map.Entry<String, Object> me : persistenceConfiguration.getPersistenceProperties().entrySet()) {
120                            if (me.getKey() == null) // don't know if null keys can ever occur, but better play safe
121                                    continue;
122    
123                            for (String prefix : CUMULUS4J_FORWARD_PROPERTY_PREFIXES) {
124                                    if (me.getKey().startsWith(prefix)) {
125                                            String propKey = me.getKey().substring(CUMULUS4J_PROPERTY_PREFIX.length());
126                                            backendProperties.put(propKey.toLowerCase(Locale.ENGLISH), me.getValue());
127                                    }
128                            }
129    
130                            for (String prefix : CUMULUS4J_INDEX_FORWARD_PROPERTY_PREFIXES) {
131                                    if (me.getKey().startsWith(prefix)) {
132                                            String propKey = me.getKey().substring(CUMULUS4J_INDEX_PROPERTY_PREFIX.length());
133                                            if (backendIndexProperties == null) {
134                                                    backendIndexProperties = new HashMap<String, Object>(backendProperties);
135                                            }
136                                            backendIndexProperties.put(propKey.toLowerCase(Locale.ENGLISH), me.getValue());
137                                    }
138                            }
139                    }
140    
141                    // The password might be encrypted, but the getConnectionPassword(...) method decrypts it.
142                    String pw = storeMgr.getConnectionPassword();
143                    if (pw != null) {
144                            backendProperties.put("datanucleus.ConnectionPassword".toLowerCase(Locale.ENGLISH), pw);
145                    }
146    
147                    // This block is an alternative to getting Extent of each Cumulus4j schema class
148    /*              StringBuffer classNameStr = new StringBuffer();
149                    classNameStr.append(ClassMeta.class.getName()).append(",");
150                    classNameStr.append(DataEntry.class.getName()).append(",");
151                    classNameStr.append(FieldMeta.class.getName()).append(",");
152                    classNameStr.append(IndexEntryContainerSize.class.getName()).append(",");
153                    classNameStr.append(Sequence.class.getName());
154                    PluginManager pluginMgr = storeMgr.getNucleusContext().getPluginManager();
155                    ConfigurationElement[] elems = pluginMgr.getConfigurationElementsForExtension(
156                                    "org.cumulus4j.store.index_mapping", null, null);
157                    if (elems != null && elems.length > 0) {
158                            HashSet<Class> initialisedClasses = new HashSet<Class>();
159                            for (int i=0;i<elems.length;i++) {
160                                    String indexTypeName = elems[i].getAttribute("index-entry-type");
161                                    Class cls = pluginMgr.loadClass("org.cumulus4j.store.index_mapping", indexTypeName);
162                                    if (!initialisedClasses.contains(cls)) {
163                                            initialisedClasses.add(cls);
164                                            classNameStr.append(",").append(indexTypeName);
165                                    }
166                            }
167                    }
168                    cumulus4jBackendProperties.put("datanucleus.autostartmechanism", "Classes");
169                    cumulus4jBackendProperties.put("datanucleus.autostartclassnames", classNameStr.toString());*/
170    
171                    // PMF for data (and optionally index)
172                    if (backendIndexProperties == null) {
173                            NucleusLogger.GENERAL.debug("Creating PMF for Data+Index with the following properties : "+StringUtils.mapToString(backendProperties));
174                    }
175                    else {
176                            NucleusLogger.GENERAL.debug("Creating PMF for Data with the following properties : "+StringUtils.mapToString(backendProperties));
177                    }
178                    pmf = JDOHelper.getPersistenceManagerFactory(backendProperties);
179    
180                    // initialise meta-data (which partially tests it)
181                    PersistenceManager pm = pmf.getPersistenceManager();
182                    try {
183                            // Class structure meta-data
184                            pm.getExtent(ClassMeta.class);
185                            pm.getExtent(FieldMeta.class);
186                            pm.getExtent(EmbeddedClassMeta.class);
187                            pm.getExtent(EmbeddedFieldMeta.class);
188    
189                            // Data
190                            pm.getExtent(DataEntry.class);
191    
192                            // Sequence for ID generation
193                            pm.getExtent(Sequence2.class);
194    
195                            // Mapping for encryption settings (encryption algorithm, mode, padding, MAC, etc.
196                            // are mapped to a number which reduces the size of each record)
197                            pm.getExtent(EncryptionCoordinateSet.class);
198    
199                            // versioning of datastore structure
200                            pm.getExtent(DatastoreVersion.class);
201    
202                            if (backendIndexProperties == null) {
203                                    // Index
204                                    initialiseIndexMetaData(pm, storeMgr);
205                            }
206                    } finally {
207                            pm.close();
208                    }
209    
210                    if (backendIndexProperties != null) {
211                            // PMF for index data
212                            NucleusLogger.GENERAL.debug("Creating PMF for Index data with the following properties : "+StringUtils.mapToString(backendIndexProperties));
213                            pmfIndex = JDOHelper.getPersistenceManagerFactory(backendIndexProperties);
214    
215                            PersistenceManager pmIndex = pmfIndex.getPersistenceManager();
216                            try {
217                                    // Class structure meta-data
218                                    pmIndex.getExtent(ClassMeta.class);
219                                    pmIndex.getExtent(FieldMeta.class);
220    
221                                    // Index
222                                    initialiseIndexMetaData(pmIndex, storeMgr);
223    
224                                    // versioning of datastore structure
225                                    pm.getExtent(DatastoreVersion.class);
226                            } finally {
227                                    pmIndex.close();
228                            }
229                    }
230            }
231    
232            private static void initialiseIndexMetaData(PersistenceManager pm, StoreManager storeMgr)
233            {
234                    // While it is not necessary to initialise the meta-data now (can be done lazily,
235                    // when the index is used), it is still better as it prevents delays when the
236                    // data is persisted.
237                    // Furthermore, if the underlying database uses transactional DDL (like PostgreSQL, MSSQL
238                    // and others), and a separate JDBC connection is used for DDL (like it must be in
239                    // a JEE server), it is essentially required to initialise the meta-data in a separate
240                    // transaction before actually using the tables.
241                    pm.getExtent(IndexEntryContainerSize.class);
242    
243                    Set<Class<? extends IndexEntry>> indexEntryClasses = ((Cumulus4jStoreManager)storeMgr).getIndexFactoryRegistry().getIndexEntryClasses();
244                    for (Class<? extends IndexEntry> indexEntryClass : indexEntryClasses) {
245                            pm.getExtent(indexEntryClass);
246                    }
247    
248    //              PluginManager pluginMgr = storeMgr.getNucleusContext().getPluginManager();
249    //              ConfigurationElement[] elems = pluginMgr.getConfigurationElementsForExtension(
250    //                              "org.cumulus4j.store.index_mapping", null, null);
251    //              if (elems != null && elems.length > 0) {
252    //                      HashSet<Class<?>> initialisedClasses = new HashSet<Class<?>>();
253    //                      for (int i=0;i<elems.length;i++) {
254    //                              String indexTypeName = elems[i].getAttribute("index-entry-type");
255    //                              Class<?> cls = pluginMgr.loadClass("org.cumulus4j.store.index_mapping", indexTypeName);
256    //                              if (!initialisedClasses.contains(cls)) {
257    //                                      initialisedClasses.add(cls);
258    //                                      pm.getExtent(cls);
259    //                              }
260    //                      }
261    //              }
262            }
263    
264            public PersistenceManagerFactory getPMFData() {
265                    return pmf;
266            }
267    
268            public PersistenceManagerFactory getPMFIndex() {
269                    return pmfIndex;
270            }
271    
272            @Override
273            public ManagedConnection createManagedConnection(Object poolKey, @SuppressWarnings("rawtypes") Map transactionOptions)
274            {
275                    return new Cumulus4jManagedConnection(poolKey, transactionOptions);
276            }
277    
278            /**
279             * Cumulus4j-specific {@link ManagedConnection} implementation. Avoid to access this specific class whenever possible!
280             * @author mschulze
281             */
282            class Cumulus4jManagedConnection extends AbstractManagedConnection
283            {
284                    private Object poolKey;
285    
286                    @SuppressWarnings({"rawtypes","unused"})
287                    private Map options;
288    
289                    PersistenceManagerConnection pmConnection;
290    
291                    @Override
292                    public XAResource getXAResource() {
293                            return new Cumulus4jXAResource((PersistenceManagerConnection)getConnection());
294                    }
295    
296                    public Cumulus4jManagedConnection(Object poolKey, @SuppressWarnings("rawtypes") Map options) {
297                            this.poolKey = poolKey;
298                            this.options = options;
299                    }
300    
301                    public Object getPoolKey() {
302                            return poolKey;
303                    }
304    
305                    @Override
306                    public void close() {
307                            if (pmConnection != null) {
308                                    PersistenceManager dataPM = pmConnection.getDataPM();
309                                    dataPM.close();
310                                    if (pmConnection.indexHasOwnPM()) {
311                                            PersistenceManager indexPM = pmConnection.getIndexPM();
312                                            indexPM.close();
313                                    }
314                                    pmConnection = null;
315                            }
316                    }
317    
318                    @Override
319                    public Object getConnection() {
320                            if (pmConnection == null) {
321                                    this.pmConnection = new PersistenceManagerConnection(pmf.getPersistenceManager(),
322                                                    pmfIndex != null ? pmfIndex.getPersistenceManager() : null);
323                            }
324                            return pmConnection;
325                    }
326            }
327    
328            class Cumulus4jXAResource implements XAResource {
329                    private PersistenceManagerConnection pmConnection;
330                    //        private Xid xid;
331    
332                    Cumulus4jXAResource(PersistenceManagerConnection pmConn) {
333                            this.pmConnection = pmConn;
334                    }
335    
336                    @Override
337                    public void start(Xid xid, int arg1) throws XAException {
338                            //              if (this.xid != null)
339                            //                      throw new IllegalStateException("Transaction already started! Cannot start twice!");
340    
341                            PersistenceManager dataPM = pmConnection.getDataPM();
342                            dataPM.currentTransaction().begin();
343                            if (pmConnection.indexHasOwnPM()) {
344                                    PersistenceManager indexPM = pmConnection.getIndexPM();
345                                    indexPM.currentTransaction().begin();
346                            }
347                            //              this.xid = xid;
348                    }
349    
350                    @Override
351                    public void commit(Xid xid, boolean arg1) throws XAException {
352                            //              if (this.xid == null)
353                            //                      throw new IllegalStateException("Transaction not active!");
354                            //
355                            //              if (!this.xid.equals(xid))
356                            //                      throw new IllegalStateException("Transaction mismatch! this.xid=" + this.xid + " otherXid=" + xid);
357    
358                            PersistenceManager dataPM = pmConnection.getDataPM();
359                            dataPM.currentTransaction().commit();
360                            if (pmConnection.indexHasOwnPM()) {
361                                    PersistenceManager indexPM = pmConnection.getIndexPM();
362                                    indexPM.currentTransaction().commit();
363                            }
364    
365                            //            this.xid = null;
366                    }
367    
368                    @Override
369                    public void rollback(Xid xid) throws XAException {
370                            //              if (this.xid == null)
371                            //                      throw new IllegalStateException("Transaction not active!");
372                            //
373                            //              if (!this.xid.equals(xid))
374                            //                      throw new IllegalStateException("Transaction mismatch! this.xid=" + this.xid + " otherXid=" + xid);
375    
376                            PersistenceManager dataPM = pmConnection.getDataPM();
377                            dataPM.currentTransaction().rollback();
378                            if (pmConnection.indexHasOwnPM()) {
379                                    PersistenceManager indexPM = pmConnection.getIndexPM();
380                                    indexPM.currentTransaction().rollback();
381                            }
382    
383                            //            this.xid = null;
384                    }
385    
386                    @Override
387                    public void end(Xid arg0, int arg1) throws XAException {
388                            //ignore
389                    }
390    
391                    @Override
392                    public void forget(Xid arg0) throws XAException {
393                            //ignore
394                    }
395    
396                    @Override
397                    public int getTransactionTimeout() throws XAException {
398                            return 0;
399                    }
400    
401                    @Override
402                    public boolean isSameRM(XAResource resource) throws XAException {
403                            if ((resource instanceof Cumulus4jXAResource) && pmConnection.equals(((Cumulus4jXAResource)resource).pmConnection))
404                                    return true;
405                            else
406                                    return false;
407                    }
408    
409                    @Override
410                    public int prepare(Xid arg0) throws XAException {
411                            return 0;
412                    }
413    
414                    @Override
415                    public Xid[] recover(int arg0) throws XAException {
416                            throw new XAException("Unsupported operation");
417                    }
418    
419                    @Override
420                    public boolean setTransactionTimeout(int arg0) throws XAException {
421                            return false;
422                    }
423            }
424    }