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 }