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 }