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.identity.IdentityUtils;
033    import org.datanucleus.metadata.AbstractClassMetaData;
034    import org.datanucleus.store.ExecutionContext;
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 (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    }