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