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