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