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.ArrayList;
021    import java.util.Collection;
022    import java.util.Collections;
023    import java.util.HashMap;
024    import java.util.HashSet;
025    import java.util.Map;
026    import java.util.Properties;
027    import java.util.Set;
028    import java.util.WeakHashMap;
029    
030    import javax.jdo.FetchPlan;
031    import javax.jdo.PersistenceManager;
032    
033    import org.cumulus4j.store.model.ClassMeta;
034    import org.cumulus4j.store.model.DataEntry;
035    import org.cumulus4j.store.model.FieldMeta;
036    import org.cumulus4j.store.model.FieldMetaRole;
037    import org.cumulus4j.store.model.IndexEntryFactoryRegistry;
038    import org.datanucleus.ClassLoaderResolver;
039    import org.datanucleus.NucleusContext;
040    import org.datanucleus.api.jdo.JDOPersistenceManagerFactory;
041    import org.datanucleus.identity.OID;
042    import org.datanucleus.identity.SCOID;
043    import org.datanucleus.metadata.AbstractClassMetaData;
044    import org.datanucleus.metadata.AbstractMemberMetaData;
045    import org.datanucleus.store.AbstractStoreManager;
046    import org.datanucleus.store.ExecutionContext;
047    import org.datanucleus.store.connection.ManagedConnection;
048    import org.datanucleus.store.schema.SchemaAwareStoreManager;
049    import org.slf4j.Logger;
050    import org.slf4j.LoggerFactory;
051    
052    /**
053     * Store Manager for Cumulus4J operation.
054     * This StoreManager handles a backend StoreManager for the persistence to the chosen datastore, and optionally
055     * a second backend StoreManager for the persistence of index data to the chosen index datastore.
056     * The user will persist objects of their own classes, and these will be translated into the persistence of
057     * DataEntry, ClassMeta, FieldMeta for the data, as well as various IndexXXX types.
058     *
059     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
060     */
061    public class Cumulus4jStoreManager extends AbstractStoreManager implements SchemaAwareStoreManager
062    {
063            private static final Logger logger = LoggerFactory.getLogger(Cumulus4jStoreManager.class);
064    
065            /** Extension key for marking field as not queryable */
066            public static final String CUMULUS4J_QUERYABLE = "cumulus4j-queryable";
067    
068    //      private static final SequenceMetaData SEQUENCE_META_DATA_DATA_ENTRY;
069    //      static {
070    //              SEQUENCE_META_DATA_DATA_ENTRY = new SequenceMetaData(DataEntry.class.getName(), SequenceStrategy.NONTRANSACTIONAL.toString());
071    //              SEQUENCE_META_DATA_DATA_ENTRY.setAllocationSize(100);
072    //              SEQUENCE_META_DATA_DATA_ENTRY.setDatastoreSequence(DataEntry.class.getName());
073    //      }
074    
075            private Map<Class<?>, ClassMeta> class2classMeta = Collections.synchronizedMap(new HashMap<Class<?>, ClassMeta>());
076    
077            /**
078             * For every class, we keep a set of all known sub-classes (all inheritance-levels down). Note, that the class in
079             * the map-key is contained in the Set (in the map-value).
080             */
081            private Map<Class<?>, Set<Class<?>>> class2subclasses = Collections.synchronizedMap(new HashMap<Class<?>, Set<Class<?>>>());
082    
083            private EncryptionHandler encryptionHandler;
084            private EncryptionCoordinateSetManager encryptionCoordinateSetManager;
085    
086            private IndexEntryFactoryRegistry indexFactoryRegistry;
087    
088            public Cumulus4jStoreManager(ClassLoaderResolver clr, NucleusContext nucleusContext, Map<String, Object> props)
089            {
090                    super("cumulus4j", clr, nucleusContext, props);
091    
092                    logger.info("====================== Cumulus4j ======================");
093                    String bundleName = "org.cumulus4j.store";
094        String version = nucleusContext.getPluginManager().getVersionForBundle(bundleName);
095        logger.info("Bundle: " + bundleName + " - Version: " + version);
096                    logger.info("=======================================================");
097    
098                    indexFactoryRegistry = new IndexEntryFactoryRegistry(this);
099                    encryptionHandler = new EncryptionHandler();
100                    encryptionCoordinateSetManager = new EncryptionCoordinateSetManager();
101                    persistenceHandler = new Cumulus4jPersistenceHandler(this);
102            }
103    
104            public EncryptionHandler getEncryptionHandler() {
105                    return encryptionHandler;
106            }
107    
108            public EncryptionCoordinateSetManager getEncryptionCoordinateSetManager() {
109                    return encryptionCoordinateSetManager;
110            }
111    
112            public IndexEntryFactoryRegistry getIndexFactoryRegistry() {
113                    return indexFactoryRegistry;
114            }
115    
116            /**
117             * Get the persistent meta-data of a certain class. This persistent meta-data is primarily used for efficient
118             * mapping using long-identifiers instead of fully qualified class names.
119             *
120             * @param ec
121             * @param clazz the {@link Class} for which to query the meta-data.
122             * @return the meta-data. Never returns <code>null</code>.
123             */
124            public ClassMeta getClassMeta(ExecutionContext ec, Class<?> clazz)
125            {
126                    ClassMeta result = class2classMeta.get(clazz);
127                    if (result != null) {
128                            return result;
129                    }
130    
131                    ManagedConnection mconn = this.getConnection(ec);
132                    try {
133                            PersistenceManagerConnection pmConn = (PersistenceManagerConnection)mconn.getConnection();
134                            PersistenceManager pm = pmConn.getDataPM();
135    
136                            synchronized (this) { // Synchronise in case we have data and index backends
137                                    // Register the class
138                                    pm.getFetchPlan().setGroup(FetchPlan.ALL);
139                                    result = registerClass(ec, pm, clazz);
140    
141                                    // Detach the class in order to cache only detached objects. Make sure fetch-plan detaches all
142                                    pm.getFetchPlan().setGroup(FetchPlan.ALL);
143                                    pm.getFetchPlan().setMaxFetchDepth(-1);
144                                    result = pm.detachCopy(result);
145    
146                                    if (pmConn.indexHasOwnPM()) {
147                                            // Replicate ClassMeta+FieldMeta to Index datastore
148                                            PersistenceManager pmIndex = pmConn.getIndexPM();
149                                            pmIndex.getFetchPlan().setGroup(FetchPlan.ALL);
150                                            pmIndex.getFetchPlan().setMaxFetchDepth(-1);
151                                            pmIndex.makePersistent(result);
152                                    }
153                            }
154    
155                            class2classMeta.put(clazz, result);
156    
157                            // register in class2subclasses-map
158                            Set<Class<?>> currentSubclasses = new HashSet<Class<?>>();
159                            Class<?> c = clazz;
160                            ClassMeta cm = result;
161                            while (cm != null) {
162                                    currentSubclasses.add(c);
163    
164                                    Set<Class<?>> subclasses;
165                                    synchronized (class2subclasses) {
166                                            subclasses = class2subclasses.get(c);
167                                            if (subclasses == null) {
168                                                    subclasses = Collections.synchronizedSet(new HashSet<Class<?>>());
169                                                    class2subclasses.put(c, subclasses);
170                                            }
171                                    }
172    
173                                    subclasses.addAll(currentSubclasses);
174    
175                                    c = c.getSuperclass();
176                                    cm = cm.getSuperClassMeta();
177                                    if (cm != null) {
178                                            if (c == null)
179                                                    throw new IllegalStateException("c == null && cm.className == " + cm.getClassName());
180    
181                                            if (!cm.getClassName().equals(c.getName()))
182                                                    throw new IllegalStateException("cm.className != c.name :: cm.className=" + cm.getClassName() + " c.name=" + c.getName());
183    
184                                            // Store the super-class-meta-data for optimisation reasons (not necessary, but [hopefully] better).
185                                            class2classMeta.put(c, cm);
186                                    }
187                            }
188                    } finally {
189                            mconn.release();
190                    }
191    
192                    return result;
193            }
194    
195            private ClassMeta registerClass(ExecutionContext ec, PersistenceManager pm, Class<?> clazz)
196            {
197                    AbstractClassMetaData dnClassMetaData = getMetaDataManager().getMetaDataForClass(clazz, ec.getClassLoaderResolver());
198                    if (dnClassMetaData == null)
199                            throw new IllegalArgumentException("The class " + clazz.getName() + " does not have persistence-meta-data! Is it persistence-capable? Is it enhanced?");
200    
201                    ClassMeta classMeta = ClassMeta.getClassMeta(pm, clazz, false);
202                    boolean classExists = (classMeta != null);
203                    if (!classExists) {
204                            classMeta = new ClassMeta(clazz);
205                    }
206    
207                    Class<?> superclass = clazz.getSuperclass();
208                    if (superclass != null && getMetaDataManager().hasMetaDataForClass(superclass.getName())) {
209                            ClassMeta superClassMeta = registerClass(ec, pm, superclass);
210                            classMeta.setSuperClassMeta(superClassMeta);
211                    }
212    
213                    Set<String> persistentMemberNames = new HashSet<String>();
214                    for (AbstractMemberMetaData memberMetaData : dnClassMetaData.getManagedMembers()) {
215                            if (!memberMetaData.isFieldToBePersisted())
216                                    continue;
217    
218                            persistentMemberNames.add(memberMetaData.getName());
219                            int dnAbsoluteFieldNumber = memberMetaData.getAbsoluteFieldNumber();
220    
221                            // register primary field-meta
222                            FieldMeta primaryFieldMeta = classMeta.getFieldMeta(memberMetaData.getName());
223                            if (primaryFieldMeta == null) {
224                                    // adding field that's so far unknown
225                                    primaryFieldMeta = new FieldMeta(classMeta, memberMetaData.getName());
226                                    classMeta.addFieldMeta(primaryFieldMeta);
227                            }
228                            primaryFieldMeta.setDataNucleusAbsoluteFieldNumber(dnAbsoluteFieldNumber);
229    
230                            if (memberMetaData.hasCollection()) {
231                                    // register "collection" field-meta, if appropriate
232                                    primaryFieldMeta.removeAllSubFieldMetasExcept(FieldMetaRole.collectionElement);
233                                    FieldMeta subFieldMeta = primaryFieldMeta.getSubFieldMeta(FieldMetaRole.collectionElement);
234                                    if (subFieldMeta == null) {
235                                            // adding field that's so far unknown
236                                            subFieldMeta = new FieldMeta(primaryFieldMeta, FieldMetaRole.collectionElement);
237                                            primaryFieldMeta.addSubFieldMeta(subFieldMeta);
238                                    }
239                            }
240                            else if (memberMetaData.hasArray()) {
241                                    // register "array" field-meta, if appropriate
242                                    // TODO shouldn't we handle it exactly as a collection, including reusing 'FieldMetaRole.collectionElement' for this case?
243                                    primaryFieldMeta.removeAllSubFieldMetasExcept(FieldMetaRole.arrayElement);
244                                    FieldMeta subFieldMeta = primaryFieldMeta.getSubFieldMeta(FieldMetaRole.arrayElement);
245                                    if (subFieldMeta == null) {
246                                            // adding field that's so far unknown
247                                            subFieldMeta = new FieldMeta(primaryFieldMeta, FieldMetaRole.arrayElement);
248                                            primaryFieldMeta.addSubFieldMeta(subFieldMeta);
249                                    }
250                            }
251                            else if (memberMetaData.hasMap()) {
252                                    // register "map" field-meta, if appropriate
253                                    primaryFieldMeta.removeAllSubFieldMetasExcept(FieldMetaRole.mapKey, FieldMetaRole.mapValue);
254    
255                                    FieldMeta subFieldMeta = primaryFieldMeta.getSubFieldMeta(FieldMetaRole.mapKey);
256                                    if (subFieldMeta == null) {
257                                            // adding field that's so far unknown
258                                            subFieldMeta = new FieldMeta(primaryFieldMeta, FieldMetaRole.mapKey);
259                                            primaryFieldMeta.addSubFieldMeta(subFieldMeta);
260                                    }
261    
262                                    subFieldMeta = primaryFieldMeta.getSubFieldMeta(FieldMetaRole.mapValue);
263                                    if (subFieldMeta == null) {
264                                            // adding field that's so far unknown
265                                            subFieldMeta = new FieldMeta(primaryFieldMeta, FieldMetaRole.mapValue);
266                                            primaryFieldMeta.addSubFieldMeta(subFieldMeta);
267                                    }
268                            }
269                            else
270                                    primaryFieldMeta.removeAllSubFieldMetasExcept();
271                    }
272    
273                    for (FieldMeta fieldMeta : new ArrayList<FieldMeta>(classMeta.getFieldMetas())) {
274                            if (persistentMemberNames.contains(fieldMeta.getFieldName()))
275                                    continue;
276    
277                            // The field is not in the class anymore => remove its persistent reference.
278                            classMeta.removeFieldMeta(fieldMeta);
279                    }
280    
281                    if (!classExists) {
282                        // Persist the new class and its fields in one call, minimising updates
283                        pm.makePersistent(classMeta);
284                    }
285                    pm.flush(); // Get exceptions as soon as possible by forcing a flush here
286    
287                    return classMeta;
288            }
289    
290            private Map<Object, String> objectID2className = Collections.synchronizedMap(new WeakHashMap<Object, String>());
291    
292            /**
293             * Store the association between an objectID and the class-name of the corresponding persistable object in
294             * a {@link WeakHashMap}. This is used for performance optimization of
295             * {@link #getClassNameForObjectID(Object, ClassLoaderResolver, ExecutionContext)}.
296             */
297            public void setClassNameForObjectID(Object id, String className)
298            {
299                    objectID2className.put(id, className);
300            }
301    
302            @Override
303            public String getClassNameForObjectID(Object id, ClassLoaderResolver clr, ExecutionContext ec)
304            {
305                    if (id == null) {
306                            return null;
307                    }
308    
309                    String className = objectID2className.get(id);
310                    if (className != null) {
311                            return className;
312                    }
313    
314                    if (id instanceof SCOID) {
315                            // Object is a SCOID
316                            className = ((SCOID) id).getSCOClass();
317                    }
318                    else if (id instanceof OID) {
319                            // Object is an OID
320                            className = ((OID)id).getPcClass();
321                    }
322                    else if (getApiAdapter().isSingleFieldIdentity(id)) {
323                            // Using SingleFieldIdentity so can assume that object is of the target class
324                            className = getApiAdapter().getTargetClassNameForSingleFieldIdentity(id);
325                    }
326                    else {
327                            // Application identity with user PK class, so find all using this PK
328                            Collection<AbstractClassMetaData> cmds = getMetaDataManager().getClassMetaDataWithApplicationId(id.getClass().getName());
329                            if (cmds != null) {
330                                    if (cmds.size() == 1) {
331                                            className = cmds.iterator().next().getFullClassName();
332                                    }
333                                    else {
334                                            ManagedConnection mconn = this.getConnection(ec);
335                                            try {
336                                                    PersistenceManagerConnection pmConn = (PersistenceManagerConnection)mconn.getConnection();
337                                                    PersistenceManager pmData = pmConn.getDataPM();
338                                                    String objectIDString = id.toString();
339                                                    for (AbstractClassMetaData cmd : cmds) {
340                                                            Class<?> clazz = clr.classForName(cmd.getFullClassName());
341                                                            ClassMeta classMeta = getClassMeta(ec, clazz);
342                                                            DataEntry dataEntry = DataEntry.getDataEntry(pmData, classMeta, objectIDString);
343                                                            if (dataEntry != null) {
344                                                                    className = cmd.getFullClassName();
345                                                            }
346                                                    }
347                                            } finally {
348                                                    mconn.release();
349                                            }
350                                    }
351                            }
352                    }
353    
354                    if (className != null) {
355                            objectID2className.put(id, className);
356                    }
357    
358                    return className;
359            }
360    
361    //      public long nextDataEntryID(ExecutionContext executionContext)
362    //      {
363    //              NucleusSequence nucleusSequence = getNucleusSequence(executionContext, SEQUENCE_META_DATA_DATA_ENTRY);
364    //              return nucleusSequence.nextValue();
365    //      }
366    
367            @Override
368            protected String getStrategyForNative(AbstractClassMetaData cmd, int absFieldNumber) {
369                    return "increment";
370    //              AbstractMemberMetaData mmd = cmd.getMetaDataForManagedMemberAtAbsolutePosition(absFieldNumber);
371    //              if (String.class.isAssignableFrom(mmd.getType()) || UUID.class.isAssignableFrom(mmd.getType()))
372    //                      return "uuid-hex";
373    //              else
374    //                      return "increment";
375            }
376    
377            @Override
378            public void createSchema(Set<String> classNames, Properties props) {
379                    Cumulus4jConnectionFactory cf =
380                            (Cumulus4jConnectionFactory) connectionMgr.lookupConnectionFactory(txConnectionFactoryName);
381                    JDOPersistenceManagerFactory pmfData = (JDOPersistenceManagerFactory) cf.getPMFData();
382                    JDOPersistenceManagerFactory pmfIndex = (JDOPersistenceManagerFactory) cf.getPMFIndex();
383                    if (pmfData.getNucleusContext().getStoreManager() instanceof SchemaAwareStoreManager) {
384                            // Create Cumulus4J "Data" (plus "Index" if not separate) schema
385                            SchemaAwareStoreManager schemaMgr = (SchemaAwareStoreManager) pmfData.getNucleusContext().getStoreManager();
386                            Set<String> cumulus4jClassNames = new HashSet<String>();
387                            Collection<Class> pmfClasses = pmfData.getManagedClasses();
388                            for (Class cls : pmfClasses) {
389                                    cumulus4jClassNames.add(cls.getName());
390                            }
391                            schemaMgr.createSchema(cumulus4jClassNames, new Properties());
392                    }
393                    if (pmfIndex != null && pmfIndex.getNucleusContext().getStoreManager() instanceof SchemaAwareStoreManager) {
394                            // Create Cumulus4J "Index" schema
395                            SchemaAwareStoreManager schemaMgr = (SchemaAwareStoreManager) pmfIndex.getNucleusContext().getStoreManager();
396                            Set<String> cumulus4jClassNames = new HashSet<String>();
397                            Collection<Class> pmfClasses = pmfIndex.getManagedClasses();
398                            for (Class cls : pmfClasses) {
399                                    cumulus4jClassNames.add(cls.getName());
400                            }
401                            schemaMgr.createSchema(cumulus4jClassNames, new Properties());
402                    }
403            }
404    
405            @Override
406            public void deleteSchema(Set<String> classNames, Properties props) {
407                    Cumulus4jConnectionFactory cf =
408                            (Cumulus4jConnectionFactory) connectionMgr.lookupConnectionFactory(txConnectionFactoryName);
409                    JDOPersistenceManagerFactory pmfData = (JDOPersistenceManagerFactory) cf.getPMFData();
410                    JDOPersistenceManagerFactory pmfIndex = (JDOPersistenceManagerFactory) cf.getPMFIndex();
411                    if (pmfData.getNucleusContext().getStoreManager() instanceof SchemaAwareStoreManager) {
412                            SchemaAwareStoreManager schemaMgr = (SchemaAwareStoreManager) pmfData.getNucleusContext().getStoreManager();
413                            Set<String> cumulus4jClassNames = new HashSet<String>();
414                            Collection<Class> pmfClasses = pmfData.getManagedClasses();
415                            for (Class cls : pmfClasses) {
416                                    cumulus4jClassNames.add(cls.getName());
417                            }
418                            schemaMgr.deleteSchema(cumulus4jClassNames, new Properties());
419                    }
420                    if (pmfIndex != null && pmfIndex.getNucleusContext().getStoreManager() instanceof SchemaAwareStoreManager) {
421                            SchemaAwareStoreManager schemaMgr = (SchemaAwareStoreManager) pmfIndex.getNucleusContext().getStoreManager();
422                            Set<String> cumulus4jClassNames = new HashSet<String>();
423                            Collection<Class> pmfClasses = pmfIndex.getManagedClasses();
424                            for (Class cls : pmfClasses) {
425                                    cumulus4jClassNames.add(cls.getName());
426                            }
427                            schemaMgr.deleteSchema(cumulus4jClassNames, new Properties());
428                    }
429            }
430    
431            @Override
432            public void validateSchema(Set<String> classNames, Properties props) {
433                    Cumulus4jConnectionFactory cf =
434                            (Cumulus4jConnectionFactory) connectionMgr.lookupConnectionFactory(txConnectionFactoryName);
435                    JDOPersistenceManagerFactory pmfData = (JDOPersistenceManagerFactory) cf.getPMFData();
436                    JDOPersistenceManagerFactory pmfIndex = (JDOPersistenceManagerFactory) cf.getPMFIndex();
437                    if (pmfData.getNucleusContext().getStoreManager() instanceof SchemaAwareStoreManager) {
438                            SchemaAwareStoreManager schemaMgr = (SchemaAwareStoreManager) pmfData.getNucleusContext().getStoreManager();
439                            Set<String> cumulus4jClassNames = new HashSet<String>();
440                            Collection<Class> pmfClasses = pmfData.getManagedClasses();
441                            for (Class cls : pmfClasses) {
442                                    cumulus4jClassNames.add(cls.getName());
443                            }
444                            schemaMgr.validateSchema(cumulus4jClassNames, new Properties());
445                    }
446                    if (pmfIndex != null && pmfIndex.getNucleusContext().getStoreManager() instanceof SchemaAwareStoreManager) {
447                            SchemaAwareStoreManager schemaMgr = (SchemaAwareStoreManager) pmfIndex.getNucleusContext().getStoreManager();
448                            Set<String> cumulus4jClassNames = new HashSet<String>();
449                            Collection<Class> pmfClasses = pmfIndex.getManagedClasses();
450                            for (Class cls : pmfClasses) {
451                                    cumulus4jClassNames.add(cls.getName());
452                            }
453                            schemaMgr.validateSchema(cumulus4jClassNames, new Properties());
454                    }
455            }
456    }