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.Arrays;
021    import java.util.Map;
022    
023    import javax.jdo.PersistenceManager;
024    
025    import org.cumulus4j.store.crypto.CryptoContext;
026    import org.cumulus4j.store.fieldmanager.FetchFieldManager;
027    import org.cumulus4j.store.fieldmanager.StoreFieldManager;
028    import org.cumulus4j.store.model.ClassMeta;
029    import org.cumulus4j.store.model.DataEntry;
030    import org.cumulus4j.store.model.FieldMeta;
031    import org.cumulus4j.store.model.ObjectContainer;
032    import org.datanucleus.exceptions.NucleusObjectNotFoundException;
033    import org.datanucleus.metadata.AbstractClassMetaData;
034    import org.datanucleus.metadata.AbstractMemberMetaData;
035    import org.datanucleus.store.AbstractPersistenceHandler;
036    import org.datanucleus.store.ExecutionContext;
037    import org.datanucleus.store.ObjectProvider;
038    import org.datanucleus.store.connection.ManagedConnection;
039    import org.slf4j.Logger;
040    import org.slf4j.LoggerFactory;
041    
042    /**
043     * Handler for all persistence calls from the StoreManager, communicating with the backend datastore(s).
044     * Manages all inserts/updates/deletes/fetches/locates of the users own objects and translates them
045     * into inserts/updates/deletes/fetches/locates of Cumulus4J model objects.
046     */
047    public class Cumulus4jPersistenceHandler extends AbstractPersistenceHandler
048    {
049            private static final Logger logger = LoggerFactory.getLogger(Cumulus4jPersistenceHandler.class);
050    
051            private Cumulus4jStoreManager storeManager;
052            private EncryptionCoordinateSetManager encryptionCoordinateSetManager;
053            private EncryptionHandler encryptionHandler;
054    
055            private IndexEntryAction addIndexEntry;
056            private IndexEntryAction removeIndexEntry;
057    
058            public Cumulus4jPersistenceHandler(Cumulus4jStoreManager storeManager) {
059                    if (storeManager == null)
060                            throw new IllegalArgumentException("storeManager == null");
061    
062                    this.storeManager = storeManager;
063                    this.encryptionCoordinateSetManager = storeManager.getEncryptionCoordinateSetManager();
064                    this.encryptionHandler = storeManager.getEncryptionHandler();
065    
066                    this.addIndexEntry = new IndexEntryAction.Add(this);
067                    this.removeIndexEntry = new IndexEntryAction.Remove(this);
068            }
069    
070            public Cumulus4jStoreManager getStoreManager() {
071                    return storeManager;
072            }
073    
074            @Override
075            public void close() {
076                    // No resources require to be closed here.
077            }
078    
079            @Override
080            public void deleteObject(ObjectProvider op) {
081                    // Check if read-only so update not permitted
082                    storeManager.assertReadOnlyForUpdateOfObject(op);
083    
084                    ExecutionContext ec = op.getExecutionContext();
085                    ManagedConnection mconn = storeManager.getConnection(ec);
086                    try {
087                            PersistenceManagerConnection pmConn = (PersistenceManagerConnection)mconn.getConnection();
088                            PersistenceManager pmData = pmConn.getDataPM();
089                            CryptoContext cryptoContext = new CryptoContext(encryptionCoordinateSetManager, ec, pmConn);
090    
091                            Object object = op.getObject();
092                            Object objectID = op.getExternalObjectId();
093                            String objectIDString = objectID.toString();
094                            ClassMeta classMeta = storeManager.getClassMeta(ec, object.getClass());
095                            DataEntry dataEntry = DataEntry.getDataEntry(pmData, classMeta, objectIDString);
096                            //                      if (dataEntry == null)
097                            //                              throw new NucleusObjectNotFoundException("Object does not exist in datastore: class=" + classMeta.getClassName() + " oid=" + objectIDString);
098    
099                            if (dataEntry != null) {
100                                    // decrypt object-container in order to identify index entries for deletion
101                                    ObjectContainer objectContainer = encryptionHandler.decryptDataEntry(cryptoContext, dataEntry);
102                                    AbstractClassMetaData dnClassMetaData = storeManager.getMetaDataManager().getMetaDataForClass(object.getClass(), ec.getClassLoaderResolver());
103    
104                                    for (Map.Entry<Long, ?> me : objectContainer.getFieldID2value().entrySet()) {
105                                            long fieldID = me.getKey();
106                                            Object fieldValue = me.getValue();
107                                            FieldMeta fieldMeta = classMeta.getFieldMeta(fieldID);
108                                            AbstractMemberMetaData dnMemberMetaData = dnClassMetaData.getMetaDataForManagedMemberAtAbsolutePosition(fieldMeta.getDataNucleusAbsoluteFieldNumber());
109    
110                                            // sanity checks
111                                            if (dnMemberMetaData == null)
112                                                    throw new IllegalStateException("dnMemberMetaData == null!!! class == \"" + classMeta.getClassName() + "\" fieldMeta.dataNucleusAbsoluteFieldNumber == " + fieldMeta.getDataNucleusAbsoluteFieldNumber() + " fieldMeta.fieldName == \"" + fieldMeta.getFieldName() + "\"");
113    
114                                            if (!fieldMeta.getFieldName().equals(dnMemberMetaData.getName()))
115                                                    throw new IllegalStateException("Meta data inconsistency!!! class == \"" + classMeta.getClassName() + "\" fieldMeta.dataNucleusAbsoluteFieldNumber == " + fieldMeta.getDataNucleusAbsoluteFieldNumber() + " fieldMeta.fieldName == \"" + fieldMeta.getFieldName() + "\" != dnMemberMetaData.name == \"" + dnMemberMetaData.getName() + "\"");
116    
117                                            removeIndexEntry.perform(cryptoContext, dataEntry.getDataEntryID(), fieldMeta, dnMemberMetaData, fieldValue);
118                                    }
119                                    pmData.deletePersistent(dataEntry);
120                            }
121    
122                    } finally {
123                            mconn.release();
124                    }
125            }
126    
127            @Override
128            public void fetchObject(ObjectProvider op, int[] fieldNumbers)
129            {
130                    ExecutionContext ec = op.getExecutionContext();
131                    ManagedConnection mconn = storeManager.getConnection(ec);
132                    try {
133                            PersistenceManagerConnection pmConn = (PersistenceManagerConnection)mconn.getConnection();
134                            PersistenceManager pmData = pmConn.getDataPM();
135                            CryptoContext cryptoContext = new CryptoContext(encryptionCoordinateSetManager, ec, pmConn);
136    
137                            Object object = op.getObject();
138                            Object objectID = op.getExternalObjectId();
139                            String objectIDString = objectID.toString();
140                            ClassMeta classMeta = storeManager.getClassMeta(ec, object.getClass());
141                            AbstractClassMetaData dnClassMetaData = storeManager.getMetaDataManager().getMetaDataForClass(object.getClass(), ec.getClassLoaderResolver());
142    
143                            // TODO Maybe we should load ALL *SIMPLE* fields, because the decryption happens on a per-row-level and thus
144                            // loading only some fields makes no sense performance-wise. However, maybe DataNucleus already optimizes
145                            // calls to this method. It makes definitely no sense to load 1-n- or 1-1-fields and it makes no sense to
146                            // optimize things that already are optimal. Hence we have to analyze first, how often this method is really
147                            // called in normal operation.
148                            // Marco.
149    
150                            DataEntry dataEntry = DataEntry.getDataEntry(pmData, classMeta, objectIDString);
151                            if (dataEntry == null)
152                                    throw new NucleusObjectNotFoundException("Object does not exist in datastore: class=" + classMeta.getClassName() + " oid=" + objectIDString);
153    
154                            ObjectContainer objectContainer = encryptionHandler.decryptDataEntry(cryptoContext, dataEntry);
155    
156                            op.replaceFields(fieldNumbers, new FetchFieldManager(op, cryptoContext, classMeta, dnClassMetaData, objectContainer));
157                            if (op.getVersion() == null) // null-check prevents overwriting in case this method is called multiple times (for different field-numbers) - TODO necessary?
158                                    op.setVersion(objectContainer.getVersion());
159                    } finally {
160                            mconn.release();
161                    }
162            }
163    
164            @Override
165            public Object findObject(ExecutionContext ec, Object id) {
166                    // Since we don't manage the memory instantiation of objects this just returns null.
167                    return null;
168            }
169    
170            @Override
171            public void insertObject(ObjectProvider op)
172            {
173                    // Check if read-only so update not permitted
174                    storeManager.assertReadOnlyForUpdateOfObject(op);
175    
176                    ExecutionContext ec = op.getExecutionContext();
177                    ManagedConnection mconn = storeManager.getConnection(ec);
178                    try {
179                            PersistenceManagerConnection pmConn = (PersistenceManagerConnection)mconn.getConnection();
180                            PersistenceManager pmData = pmConn.getDataPM();
181                            CryptoContext cryptoContext = new CryptoContext(encryptionCoordinateSetManager, ec, pmConn);
182    
183                            Object object = op.getObject();
184                            Object objectID = op.getExternalObjectId();
185                            ClassMeta classMeta = storeManager.getClassMeta(ec, object.getClass());
186    
187                            AbstractClassMetaData dnClassMetaData = storeManager.getMetaDataManager().getMetaDataForClass(object.getClass(), ec.getClassLoaderResolver());
188    
189                            int[] allFieldNumbers = dnClassMetaData.getAllMemberPositions();
190                            ObjectContainer objectContainer = new ObjectContainer();
191                            String objectIDString = objectID.toString();
192    
193                            // We have to persist the DataEntry before the call to provideFields(...), because the InsertFieldManager recursively
194                            // persists other fields which might back-reference (=> mapped-by) and thus need this DataEntry to already exist.
195                            // TO DO Try to make this persistent afterwards and solve the problem by only allocating the ID before [keeping it in memory] (see Cumulus4jStoreManager#nextDataEntryID(), which is commented out currently).
196                            //   Even though reducing the INSERT + UPDATE to one single INSERT in the handling of IndexEntry made
197                            //   things faster, it seems not to have a performance benefit here. But we should still look at this
198                            //   again later.
199                            // Marco.
200                            //
201                            // 2012-02-02: Refactored this because of a Heisenbug with optimistic transactions. At the same time solved
202                            // the above to do. Marco :-)
203    
204    //                      // In case we work with deferred datastore operations, the DataEntry might already have been written by
205    //                      // ObjectContainerHelper.entityToReference(...). We therefore, check, if it already exists (and update it then instead of insert).
206    //                      DataEntry dataEntry;
207    //                      dataEntry = DataEntry.getDataEntry(pmData, classMeta, objectIDString);
208    //                      if (dataEntry != null)
209    //                              logger.trace("insertObject: Found existing DataEntry for: {}", objectIDString);
210    //                      else {
211    //                              dataEntry = pmData.makePersistent(new DataEntry(classMeta, objectIDString));
212    //                              logger.trace("insertObject: Persisted DataEntry for: {}", objectIDString);
213    //                      }
214    
215                            // This performs reachability on this input object so that all related objects are persisted.
216                            op.provideFields(allFieldNumbers, new StoreFieldManager(op, pmData, classMeta, dnClassMetaData, objectContainer));
217                            objectContainer.setVersion(op.getTransactionalVersion());
218    
219                            // The DataEntry might already have been written by ObjectContainerHelper.entityToReference(...),
220                            // if it was needed for a reference. We therefore check, if it already exists (and update it then instead of insert).
221                            boolean persistDataEntry = false;
222                            DataEntry dataEntry = ObjectContainerHelper.popTemporaryReferenceDataEntry(pmData, objectIDString);
223                            if (dataEntry != null)
224                                    logger.trace("insertObject: Found temporary-reference-DataEntry for: {}", objectIDString);
225                            else {
226                                    persistDataEntry = true;
227                                    dataEntry = new DataEntry(classMeta, objectIDString);
228                                    logger.trace("insertObject: Created new DataEntry for: {}", objectIDString);
229                            }
230    
231                            // persist data
232                            encryptionHandler.encryptDataEntry(cryptoContext, dataEntry, objectContainer);
233    
234                            if (persistDataEntry) {
235                                    dataEntry = pmData.makePersistent(dataEntry);
236                                    logger.trace("insertObject: Persisted new DataEntry for: {}", objectIDString);
237                            }
238    
239                            // persist index
240                            for (Map.Entry<Long, ?> me : objectContainer.getFieldID2value().entrySet()) {
241                                    long fieldID = me.getKey();
242                                    Object fieldValue = me.getValue();
243                                    FieldMeta fieldMeta = classMeta.getFieldMeta(fieldID);
244                                    AbstractMemberMetaData dnMemberMetaData = dnClassMetaData.getMetaDataForManagedMemberAtAbsolutePosition(fieldMeta.getDataNucleusAbsoluteFieldNumber());
245    
246                                    // sanity checks
247                                    if (dnMemberMetaData == null)
248                                            throw new IllegalStateException("dnMemberMetaData == null!!! class == \"" + classMeta.getClassName() + "\" fieldMeta.dataNucleusAbsoluteFieldNumber == " + fieldMeta.getDataNucleusAbsoluteFieldNumber() + " fieldMeta.fieldName == \"" + fieldMeta.getFieldName() + "\"");
249    
250                                    if (!fieldMeta.getFieldName().equals(dnMemberMetaData.getName()))
251                                            throw new IllegalStateException("Meta data inconsistency!!! class == \"" + classMeta.getClassName() + "\" fieldMeta.dataNucleusAbsoluteFieldNumber == " + fieldMeta.getDataNucleusAbsoluteFieldNumber() + " fieldMeta.fieldName == \"" + fieldMeta.getFieldName() + "\" != dnMemberMetaData.name == \"" + dnMemberMetaData.getName() + "\"");
252    
253                                    addIndexEntry.perform(cryptoContext, dataEntry.getDataEntryID(), fieldMeta, dnMemberMetaData, fieldValue);
254                            }
255                    } finally {
256                            mconn.release();
257                    }
258            }
259    
260            @Override
261            public void locateObject(ObjectProvider op)
262            {
263                    ManagedConnection mconn = storeManager.getConnection(op.getExecutionContext());
264                    try {
265                            PersistenceManagerConnection pmConn = (PersistenceManagerConnection)mconn.getConnection();
266                            PersistenceManager pmData = pmConn.getDataPM();
267    
268                            ClassMeta classMeta = storeManager.getClassMeta(op.getExecutionContext(), op.getObject().getClass());
269                            Object objectID = op.getExternalObjectId();
270                            String objectIDString = objectID.toString();
271    
272                            DataEntry dataEntry = DataEntry.getDataEntry(pmData, classMeta, objectIDString);
273                            if (dataEntry == null)
274                                    throw new NucleusObjectNotFoundException("Object does not exist in datastore: class=" + classMeta.getClassName() + " oid=" + objectIDString);
275                    } finally {
276                            mconn.release();
277                    }
278            }
279    
280            @Override
281            public void updateObject(ObjectProvider op, int[] fieldNumbers)
282            {
283                    // Check if read-only so update not permitted
284                    storeManager.assertReadOnlyForUpdateOfObject(op);
285    
286                    ExecutionContext ec = op.getExecutionContext();
287                    ManagedConnection mconn = storeManager.getConnection(ec);
288                    try {
289                            PersistenceManagerConnection pmConn = (PersistenceManagerConnection)mconn.getConnection();
290                            PersistenceManager pmData = pmConn.getDataPM();
291                            CryptoContext cryptoContext = new CryptoContext(encryptionCoordinateSetManager, ec, pmConn);
292    
293                            Object object = op.getObject();
294                            Object objectID = op.getExternalObjectId();
295                            String objectIDString = objectID.toString();
296                            ClassMeta classMeta = storeManager.getClassMeta(ec, object.getClass());
297                            AbstractClassMetaData dnClassMetaData = storeManager.getMetaDataManager().getMetaDataForClass(object.getClass(), ec.getClassLoaderResolver());
298    
299                            DataEntry dataEntry = DataEntry.getDataEntry(pmData, classMeta, objectIDString);
300                            if (dataEntry == null)
301                                    throw new NucleusObjectNotFoundException("Object does not exist in datastore: class=" + classMeta.getClassName() + " oid=" + objectIDString);
302    
303                            long dataEntryID = dataEntry.getDataEntryID();
304    
305                            ObjectContainer objectContainerOld = encryptionHandler.decryptDataEntry(cryptoContext, dataEntry);
306                            ObjectContainer objectContainerNew = objectContainerOld.clone();
307    
308                            // This performs reachability on this input object so that all related objects are persisted
309                            op.provideFields(fieldNumbers, new StoreFieldManager(op, pmData, classMeta, dnClassMetaData, objectContainerNew));
310                            objectContainerNew.setVersion(op.getTransactionalVersion());
311    
312                            // update persistent data
313                            encryptionHandler.encryptDataEntry(cryptoContext, dataEntry, objectContainerNew);
314    
315                            // update persistent index
316                            for (int fieldNumber : fieldNumbers) {
317                                    AbstractMemberMetaData dnMemberMetaData = dnClassMetaData.getMetaDataForManagedMemberAtAbsolutePosition(fieldNumber);
318                                    if (dnMemberMetaData == null)
319                                            throw new IllegalStateException("dnMemberMetaData == null!!! class == \"" + classMeta.getClassName() + "\" fieldNumber == " + fieldNumber);
320    
321                                    if (dnMemberMetaData.getMappedBy() != null)
322                                            continue; // TODO is this sufficient to take 'mapped-by' into account?
323    
324                                    FieldMeta fieldMeta = classMeta.getFieldMeta(dnMemberMetaData.getClassName(), dnMemberMetaData.getName());
325                                    if (fieldMeta == null)
326                                            throw new IllegalStateException("fieldMeta == null!!! class == \"" + classMeta.getClassName() + "\" dnMemberMetaData.className == \"" + dnMemberMetaData.getClassName() + "\" dnMemberMetaData.name == \"" + dnMemberMetaData.getName() + "\"");
327    
328                                    Object fieldValueOld = objectContainerOld.getValue(fieldMeta.getFieldID());
329                                    Object fieldValueNew = objectContainerNew.getValue(fieldMeta.getFieldID());
330    
331                                    if (!fieldsEqual(fieldValueOld, fieldValueNew)){
332    
333                                            /*
334                                             * TODO:
335                                             * Cumulus4j throws a NullPointerException at this point when running the poleposition benchmark
336                                             * and using a list data type which has a null value to mark the end of the list.
337                                             * This null value check solves the problem but an problem when deleting the persisted
338                                             * data occurs. I have commented this out, because i have not fully analyzed this and so i am not sure
339                                             * is this is right.
340                                             * At the moment i have no more time to analyze this problem any more. :( Jan
341                                             *
342                                            if(fieldValueOld != null)*/
343                                                    removeIndexEntry.perform(cryptoContext, dataEntryID, fieldMeta, dnMemberMetaData, fieldValueOld);
344                                            addIndexEntry.perform(cryptoContext, dataEntryID, fieldMeta, dnMemberMetaData, fieldValueNew);
345                                    }
346                            }
347                    } finally {
348                            mconn.release();
349                    }
350            }
351    
352            private static boolean fieldsEqual(Object obj0, Object obj1) {
353                    if (obj0 instanceof Object[] && obj1 instanceof Object[])
354                            return obj0 == obj1 || Arrays.equals((Object[])obj0, (Object[])obj1);
355                    return obj0 == obj1 || (obj0 != null && obj0.equals(obj1));
356            }
357    }