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.HashMap;
021 import java.util.Map;
022
023 import javax.jdo.JDOHelper;
024 import javax.jdo.PersistenceManager;
025
026 import org.cumulus4j.store.crypto.CryptoContext;
027 import org.cumulus4j.store.model.ClassMeta;
028 import org.cumulus4j.store.model.DataEntry;
029 import org.cumulus4j.store.model.DataEntryDAO;
030 import org.cumulus4j.store.model.EmbeddedObjectContainer;
031 import org.cumulus4j.store.model.ObjectContainer;
032 import org.datanucleus.ExecutionContext;
033 import org.datanucleus.identity.IdentityUtils;
034 import org.datanucleus.metadata.AbstractClassMetaData;
035 import org.slf4j.Logger;
036 import org.slf4j.LoggerFactory;
037
038 /**
039 * Helper class for replacing object-references when storing a 1-1- or 1-n- or m-n-relationship
040 * inside an {@link ObjectContainer}.
041 *
042 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
043 */
044 public final class ObjectContainerHelper
045 {
046 private static final Logger logger = LoggerFactory.getLogger(ObjectContainerHelper.class);
047
048 /**
049 * If <code>false</code>, store object-ID in {@link ObjectContainer}.
050 * If <code>true</code>, store {@link DataEntry#getDataEntryID() dataEntryID} in {@link ObjectContainer}.
051 */
052 private static final boolean USE_DATA_ENTRY_ID = true;
053
054 private ObjectContainerHelper() { }
055
056 private static final class TemporaryReferenceDataEntry {
057 public CryptoContext cryptoContext;
058 public String objectID;
059 public long dataEntryID = -1;
060 public ClassMeta classMeta;
061 }
062
063 private static void registerTemporaryReferenceDataEntry(CryptoContext cryptoContext, PersistenceManager pmData, DataEntry dataEntry)
064 {
065 assertTemporaryReferenceScopeEntered(cryptoContext, pmData);
066
067 Map<String, TemporaryReferenceDataEntry> objectID2tempRefMap = temporaryReferenceDataEntryMapThreadLocal.get();
068 if (objectID2tempRefMap == null) {
069 objectID2tempRefMap = new HashMap<String, TemporaryReferenceDataEntry>();
070 temporaryReferenceDataEntryMapThreadLocal.set(objectID2tempRefMap);
071 }
072
073 TemporaryReferenceDataEntry trde = new TemporaryReferenceDataEntry();
074 trde.cryptoContext = cryptoContext;
075 trde.objectID = dataEntry.getObjectID();
076 trde.dataEntryID = dataEntry.getDataEntryID();
077 trde.classMeta = dataEntry.getClassMeta();
078
079 if (trde.dataEntryID < 0) {
080 throw new IllegalStateException("dataEntry.dataEntryID < 0 :: trde.objectID = " + trde.objectID);
081 }
082
083 objectID2tempRefMap.put(trde.objectID, trde);
084 }
085
086 private static ThreadLocal<Map<String, TemporaryReferenceDataEntry>> temporaryReferenceDataEntryMapThreadLocal = new ThreadLocal<Map<String,TemporaryReferenceDataEntry>>();
087
088 private static ThreadLocal<Integer> temporaryReferenceScopeCounterThreadLocal = new ThreadLocal<Integer>();
089
090 public static void enterTemporaryReferenceScope() {
091 Integer temporaryReferenceScopeCounter = temporaryReferenceScopeCounterThreadLocal.get();
092
093 if (temporaryReferenceScopeCounter == null) {
094 assertNoEmptyTemporaryReferenceDataEntry();
095 temporaryReferenceScopeCounter = 1;
096 }
097 else
098 temporaryReferenceScopeCounter = temporaryReferenceScopeCounter + 1;
099
100 temporaryReferenceScopeCounterThreadLocal.set(temporaryReferenceScopeCounter);
101 }
102
103 public static void exitTemporaryReferenceScope(boolean error) {
104 Integer temporaryReferenceScopeCounter = temporaryReferenceScopeCounterThreadLocal.get();
105
106 if (temporaryReferenceScopeCounter == null)
107 throw new IllegalStateException("temporaryReferenceScopeCounter == null");
108
109 temporaryReferenceScopeCounter = temporaryReferenceScopeCounter - 1;
110 if (temporaryReferenceScopeCounter.intValue() == 0) {
111 temporaryReferenceScopeCounterThreadLocal.remove();
112 if (error)
113 deleteTemporaryReferenceEmptyDataEntries();
114 else {
115 assertNoEmptyTemporaryReferenceDataEntry();
116 temporaryReferenceDataEntryMapThreadLocal.remove();
117 }
118 }
119 else {
120 if (temporaryReferenceScopeCounter.intValue() < 0)
121 throw new IllegalStateException("temporaryReferenceScopeCounter < 0");
122
123 temporaryReferenceScopeCounterThreadLocal.set(temporaryReferenceScopeCounter);
124 }
125 }
126
127 public static DataEntry getTemporaryReferenceDataEntry(CryptoContext cryptoContext, PersistenceManager pmData, String objectIDString)
128 {
129 assertTemporaryReferenceScopeEntered(cryptoContext, pmData);
130
131 Map<String, TemporaryReferenceDataEntry> objectID2tempRefMap = temporaryReferenceDataEntryMapThreadLocal.get();
132 if (objectID2tempRefMap == null)
133 return null;
134
135 TemporaryReferenceDataEntry trde = objectID2tempRefMap.get(objectIDString);
136 if (trde == null)
137 return null;
138
139 DataEntry dataEntry = new DataEntryDAO(pmData, cryptoContext.getKeyStoreRefID()).getDataEntry(trde.dataEntryID); // .getDataEntry(trde.classMeta, objectIDString);
140 return dataEntry;
141 }
142
143 private static void deleteTemporaryReferenceEmptyDataEntries() {
144 Map<String, TemporaryReferenceDataEntry> objectID2tempRefMap = temporaryReferenceDataEntryMapThreadLocal.get();
145 if (objectID2tempRefMap == null || objectID2tempRefMap.isEmpty())
146 return;
147
148 for (TemporaryReferenceDataEntry trde : objectID2tempRefMap.values()) {
149 PersistenceManager pmData = trde.cryptoContext.getPersistenceManagerForData();
150 DataEntryDAO dataEntryDAO = new DataEntryDAO(pmData, trde.cryptoContext.getKeyStoreRefID());
151 DataEntry dataEntry = dataEntryDAO.getDataEntry(trde.dataEntryID);
152 if (dataEntry != null && (dataEntry.getValue() == null || dataEntry.getValue().length == 0))
153 pmData.deletePersistent(dataEntry);
154 }
155 temporaryReferenceDataEntryMapThreadLocal.remove();
156 }
157
158 private static void assertNoEmptyTemporaryReferenceDataEntry() {
159 Map<String, TemporaryReferenceDataEntry> objectID2tempRefMap = temporaryReferenceDataEntryMapThreadLocal.get();
160 if (objectID2tempRefMap == null || objectID2tempRefMap.isEmpty())
161 return;
162
163 for (TemporaryReferenceDataEntry trde : objectID2tempRefMap.values()) {
164 PersistenceManager pmData = trde.cryptoContext.getPersistenceManagerForData();
165 DataEntryDAO dataEntryDAO = new DataEntryDAO(pmData, trde.cryptoContext.getKeyStoreRefID());
166 DataEntry dataEntry = dataEntryDAO.getDataEntry(trde.dataEntryID);
167 if (dataEntry != null && (dataEntry.getValue() == null || dataEntry.getValue().length == 0))
168 throw new IllegalStateException("Found empty TemporaryReferenceDataEntry! dataEntryID=" + trde.dataEntryID
169 + " classMeta.classID=" + (trde.classMeta == null ? null : trde.classMeta.getClassID())
170 + " objectID=" + trde.objectID);
171 }
172 }
173
174 private static void assertTemporaryReferenceScopeEntered(CryptoContext cryptoContext, PersistenceManager pmData) {
175 Integer temporaryReferenceScopeCounter = temporaryReferenceScopeCounterThreadLocal.get();
176
177 if (temporaryReferenceScopeCounter == null)
178 throw new IllegalStateException("temporaryReferenceScopeCounter == null");
179
180 if (temporaryReferenceScopeCounter.intValue() < 1)
181 throw new IllegalStateException("temporaryReferenceScopeCounter < 1");
182 }
183
184 public static Object entityToReference(CryptoContext cryptoContext, PersistenceManager pmData, Object entity)
185 {
186 if (entity == null)
187 return null;
188
189 ExecutionContext ec = cryptoContext.getExecutionContext();
190 Cumulus4jStoreManager storeManager = (Cumulus4jStoreManager) ec.getStoreManager();
191 Object objectID = ec.getApiAdapter().getIdForObject(entity);
192 if (objectID == null)
193 throw new IllegalStateException("executionContext.getApiAdapter().getIdForObject(entity) returned null for " + entity);
194
195 storeManager.setClassNameForObjectID(objectID, entity.getClass().getName());
196
197 if (USE_DATA_ENTRY_ID) {
198 ClassMeta classMeta = storeManager.getClassMeta(ec, entity.getClass());
199 String objectIDString = objectID.toString();
200 Long dataEntryID = new DataEntryDAO(pmData, cryptoContext.getKeyStoreRefID()).getDataEntryID(classMeta, objectIDString);
201 if (dataEntryID == null) {
202 // Referenced entity not yet persisted => Create a temporarily empty DataEntry. It should be
203 // filled later when Cumulus4jPersistenceHandler.insertObject(...) is called for this entity.
204 //
205 // TODO If we ever stumble over empty DataEntry objects in the database, we should add a sanity check,
206 // which checks at the end of a flush(...) or commit(...) whether all of the DataEntry objects created here
207 // were actually post-processed by a call to Cumulus4jPersistenceHandler.insertObject(...). Marco :-)
208 DataEntry dataEntry = pmData.makePersistent(new DataEntry(classMeta, cryptoContext.getKeyStoreRefID(), objectIDString));
209 dataEntryID = dataEntry.getDataEntryID();
210 registerTemporaryReferenceDataEntry(cryptoContext, pmData, dataEntry);
211 logger.trace("entityToReference: Created temporary-reference-DataEntry for: {}", objectIDString);
212 // throw new IllegalStateException("DataEntry.getDataEntryID(...) returned null for entity=\"" + entity + "\" with objectID=\"" + objectID + "\"");
213 }
214
215 return dataEntryID;
216 }
217
218 return objectID;
219 }
220
221 public static Object referenceToEntity(CryptoContext cryptoContext, PersistenceManager pmData, Object reference)
222 {
223 if (reference == null)
224 return null;
225
226 ExecutionContext ec = cryptoContext.getExecutionContext();
227
228 if (USE_DATA_ENTRY_ID) {
229 DataEntry dataEntry = new DataEntryDAO(pmData, cryptoContext.getKeyStoreRefID()).getDataEntry(((Long)reference).longValue());
230 if (dataEntry != null && JDOHelper.isDeleted(dataEntry)) {
231 // Added check for deleted state because of https://sourceforge.net/tracker/?func=detail&aid=3515534&group_id=517465&atid=2102911
232 // Marco :-)
233 logger.warn("referenceToEntity: DataEntry.getDataEntry(...) returned deleted instance for dataEntryID=\"{}\"! Setting it to null.", reference);
234 dataEntry = null;
235 }
236
237 if (dataEntry == null) {
238 String message = String.format("DataEntry.getDataEntry(...) returned null for reference=\"%s\"!", reference);
239 if (((Cumulus4jPersistenceHandler)ec.getNucleusContext().getStoreManager().getPersistenceHandler()).useReferentialIntegrity())
240 throw new IllegalStateException(message);
241 else {
242 // https://sourceforge.net/tracker/?func=detail&aid=3515529&group_id=517465&atid=2102914
243 logger.warn("referenceToEntity: {} Returning null, because reference is orphaned.", message);
244 return null;
245 }
246 }
247
248 AbstractClassMetaData cmd = dataEntry.getClassMeta().getDataNucleusClassMetaData(ec);
249 return IdentityUtils.getObjectFromIdString(dataEntry.getObjectID(), cmd, ec, true);
250 }
251
252 return ec.findObject(reference, true, true, null);
253 }
254
255 public static Long referenceToDataEntryID(CryptoContext cryptoContext, PersistenceManager pmData, Object reference)
256 {
257 if (reference == null)
258 return null;
259
260 if (USE_DATA_ENTRY_ID) {
261 if (reference instanceof EmbeddedObjectContainer)
262 return null;
263
264 return (Long)reference;
265 }
266
267 ExecutionContext ec = cryptoContext.getExecutionContext();
268 Cumulus4jStoreManager storeManager = (Cumulus4jStoreManager) ec.getStoreManager();
269 String clazzName = storeManager.getClassNameForObjectID(reference, ec.getClassLoaderResolver(), ec);
270 Class<?> clazz = ec.getClassLoaderResolver().classForName(clazzName);
271 ClassMeta classMeta = storeManager.getClassMeta(ec, clazz);
272 return new DataEntryDAO(pmData, cryptoContext.getKeyStoreRefID()).getDataEntryID(classMeta, reference.toString());
273 }
274 }