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.lang.reflect.Array;
021    import java.util.Map;
022    
023    import javax.jdo.JDOHelper;
024    import javax.jdo.JDOUserException;
025    import javax.jdo.PersistenceManager;
026    import javax.jdo.Query;
027    
028    import org.cumulus4j.store.crypto.CryptoContext;
029    import org.cumulus4j.store.model.ClassMeta;
030    import org.cumulus4j.store.model.FieldMeta;
031    import org.cumulus4j.store.model.FieldMetaRole;
032    import org.cumulus4j.store.model.IndexEntry;
033    import org.cumulus4j.store.model.IndexEntryFactory;
034    import org.cumulus4j.store.model.IndexEntryFactoryRegistry;
035    import org.cumulus4j.store.model.IndexEntryObjectRelationHelper;
036    import org.cumulus4j.store.model.IndexValue;
037    import org.datanucleus.metadata.AbstractMemberMetaData;
038    import org.datanucleus.metadata.Relation;
039    import org.datanucleus.store.ExecutionContext;
040    import org.slf4j.Logger;
041    import org.slf4j.LoggerFactory;
042    
043    /**
044     * Logic to add or remove an index entry.
045     * <p>
046     * This class is thread-safe. You should normally never need to instantiate this class.
047     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
048     */
049    public abstract class IndexEntryAction
050    {
051            protected Cumulus4jPersistenceHandler persistenceHandler;
052            protected Cumulus4jStoreManager storeManager;
053            protected EncryptionHandler encryptionHandler;
054            protected IndexEntryFactoryRegistry indexEntryFactoryRegistry;
055    
056            public IndexEntryAction(Cumulus4jPersistenceHandler persistenceHandler) {
057                    if (persistenceHandler == null)
058                            throw new IllegalArgumentException("persistenceHandler == null");
059    
060                    this.persistenceHandler = persistenceHandler;
061                    this.storeManager = persistenceHandler.getStoreManager();
062                    this.encryptionHandler = storeManager.getEncryptionHandler();
063                    indexEntryFactoryRegistry = storeManager.getIndexFactoryRegistry();
064            }
065    
066            protected abstract IndexEntry getIndexEntry(
067                            IndexEntryFactory indexEntryFactory, CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Object fieldValue
068            );
069    
070            protected abstract IndexEntry getIndexEntryForObjectRelation(CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Long otherDataEntryID);
071    
072            protected abstract void _perform(CryptoContext cryptoContext, IndexEntry indexEntry, long dataEntryID);
073    
074            public void perform(CryptoContext cryptoContext, long dataEntryID, FieldMeta fieldMeta,
075                            AbstractMemberMetaData dnMemberMetaData, ClassMeta classMeta, Object fieldValue
076            )
077            {
078                    ExecutionContext ec = cryptoContext.getExecutionContext();
079                    PersistenceManager pmData = cryptoContext.getPersistenceManagerForData();
080                    PersistenceManager pmIndex = cryptoContext.getPersistenceManagerForIndex();
081                    boolean hasQueryable = dnMemberMetaData.hasExtension(Cumulus4jStoreManager.CUMULUS4J_QUERYABLE);
082                    if (hasQueryable) {
083                            String val = dnMemberMetaData.getValueForExtension(Cumulus4jStoreManager.CUMULUS4J_QUERYABLE);
084                            if (val.equalsIgnoreCase("false")) {
085                                    // Field marked as not queryable, so don't index it
086                                    return;
087                            }
088                    }
089    
090                    int relationType = dnMemberMetaData.getRelationType(cryptoContext.getExecutionContext().getClassLoaderResolver());
091                    if (Relation.NONE == relationType) {
092                            // The field contains no other persistent entity. It might contain a collection/array/map, though.
093    
094                            if (dnMemberMetaData.hasCollection() || dnMemberMetaData.hasArray()) {
095                                    FieldMetaRole role;
096                                    if (dnMemberMetaData.hasCollection())
097                                            role = FieldMetaRole.collectionElement;
098                                    else
099                                            role = FieldMetaRole.arrayElement;
100    
101                                    FieldMeta subFieldMeta = fieldMeta.getSubFieldMeta(role);
102                                    IndexEntryFactory indexEntryFactory = indexEntryFactoryRegistry.getIndexEntryFactory(ec, subFieldMeta, false);
103                                    if (fieldValue != null) {
104                                            for (int idx = 0; idx < Array.getLength(fieldValue); ++idx) {
105                                                    Object element = Array.get(fieldValue, idx);
106                                                    IndexEntry indexEntry = getIndexEntry(indexEntryFactory, cryptoContext, pmIndex, subFieldMeta, classMeta, element);
107                                                    _perform(cryptoContext, indexEntry, dataEntryID);
108                                            }
109                                    }
110    
111                                    // Add entry for the collection/array size
112                                    int containerSize = fieldValue == null ? 0 : Array.getLength(fieldValue);
113                                    IndexEntry sizeIdxEntry =
114                                            indexEntryFactoryRegistry.getIndexEntryFactoryForContainerSize().createIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, new Long(containerSize));
115                                    _perform(cryptoContext, sizeIdxEntry, dataEntryID);
116                            }
117                            else if (dnMemberMetaData.hasMap()) {
118                                    Map<?,?> fieldValueMap = (Map<?,?>) fieldValue;
119    
120                                    FieldMeta subFieldMetaKey = fieldMeta.getSubFieldMeta(FieldMetaRole.mapKey);
121                                    FieldMeta subFieldMetaValue = fieldMeta.getSubFieldMeta(FieldMetaRole.mapValue);
122                                    IndexEntryFactory indexEntryFactoryKey = indexEntryFactoryRegistry.getIndexEntryFactory(ec, subFieldMetaKey, false);
123                                    IndexEntryFactory indexEntryFactoryValue = indexEntryFactoryRegistry.getIndexEntryFactory(ec, subFieldMetaValue, false);
124    
125                                    if (fieldValueMap != null) {
126                                            for (Map.Entry<?, ?> me : fieldValueMap.entrySet()) {
127                                                    IndexEntry indexEntryKey = getIndexEntry(indexEntryFactoryKey, cryptoContext, pmIndex, subFieldMetaKey, classMeta, me.getKey());
128                                                    _perform(cryptoContext, indexEntryKey, dataEntryID);
129    
130                                                    IndexEntry indexEntryValue = getIndexEntry(indexEntryFactoryValue, cryptoContext, pmIndex, subFieldMetaValue, classMeta, me.getValue());
131                                                    _perform(cryptoContext, indexEntryValue, dataEntryID);
132                                            }
133                                    }
134    
135                                    // Add entry for the map size
136                                    int containerSize = (fieldValueMap != null ? fieldValueMap.size() : 0);
137                                    IndexEntry sizeIdxEntry =
138                                            indexEntryFactoryRegistry.getIndexEntryFactoryForContainerSize().createIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, new Long(containerSize));
139                                    _perform(cryptoContext, sizeIdxEntry, dataEntryID);
140                            }
141                            else {
142                                    IndexEntryFactory indexEntryFactory = indexEntryFactoryRegistry.getIndexEntryFactory(ec, fieldMeta, false);
143                                    IndexEntry indexEntry = getIndexEntry(indexEntryFactory, cryptoContext, pmIndex, fieldMeta, classMeta, fieldValue);
144                                    _perform(cryptoContext, indexEntry, dataEntryID);
145                            }
146                    }
147                    else if (Relation.isRelationSingleValued(relationType)) {
148                            // 1-1-relationship to another persistence-capable object.
149                            Long otherDataEntryID = ObjectContainerHelper.referenceToDataEntryID(cryptoContext, pmData, fieldValue);
150                            IndexEntry indexEntry = getIndexEntryForObjectRelation(cryptoContext, pmIndex, fieldMeta, classMeta, otherDataEntryID);
151                            _perform(cryptoContext, indexEntry, dataEntryID);
152                    }
153                    else if (Relation.isRelationMultiValued(relationType)) {
154                            // map, collection, array
155    
156                            if (dnMemberMetaData.hasMap()) { // Map.class.isAssignableFrom(dnMemberMetaData.getType())) {
157                                    Map<?,?> fieldValueMap = (Map<?,?>) fieldValue;
158    
159                                    boolean keyIsPersistent = dnMemberMetaData.getMap().keyIsPersistent();
160                                    boolean valueIsPersistent = dnMemberMetaData.getMap().valueIsPersistent();
161    
162                                    FieldMeta subFieldMetaKey = fieldMeta.getSubFieldMeta(FieldMetaRole.mapKey);
163                                    FieldMeta subFieldMetaValue = fieldMeta.getSubFieldMeta(FieldMetaRole.mapValue);
164                                    IndexEntryFactory indexEntryFactoryKey = indexEntryFactoryRegistry.getIndexEntryFactory(ec, subFieldMetaKey, false);
165                                    IndexEntryFactory indexEntryFactoryValue = indexEntryFactoryRegistry.getIndexEntryFactory(ec, subFieldMetaValue, false);
166    
167                                    if (fieldValueMap != null) {
168                                            for (Map.Entry<?, ?> me : fieldValueMap.entrySet()) {
169                                                    if (keyIsPersistent) {
170                                                            Long otherDataEntryID = ObjectContainerHelper.referenceToDataEntryID(cryptoContext, pmData, me.getKey());
171                                                            IndexEntry indexEntry = getIndexEntryForObjectRelation(cryptoContext, pmIndex, subFieldMetaKey, classMeta, otherDataEntryID);
172                                                            _perform(cryptoContext, indexEntry, dataEntryID);
173                                                    }
174                                                    else {
175                                                            IndexEntry indexEntry = getIndexEntry(indexEntryFactoryKey, cryptoContext, pmIndex, subFieldMetaKey, classMeta, me.getKey());
176                                                            _perform(cryptoContext, indexEntry, dataEntryID);
177                                                    }
178    
179                                                    if (valueIsPersistent) {
180                                                            Long otherDataEntryID = ObjectContainerHelper.referenceToDataEntryID(cryptoContext, pmData, me.getValue());
181                                                            IndexEntry indexEntry = getIndexEntryForObjectRelation(cryptoContext, pmIndex, subFieldMetaValue, classMeta, otherDataEntryID);
182                                                            _perform(cryptoContext, indexEntry, dataEntryID);
183                                                    }
184                                                    else {
185                                                            IndexEntry indexEntry = getIndexEntry(indexEntryFactoryValue, cryptoContext, pmIndex, subFieldMetaValue, classMeta, me.getValue());
186                                                            _perform(cryptoContext, indexEntry, dataEntryID);
187                                                    }
188                                            }
189                                    }
190    
191                                    // Add entry for the map size
192                                    int containerSize = (fieldValueMap != null ? fieldValueMap.size() : 0);
193                                    IndexEntry sizeIdxEntry =
194                                            indexEntryFactoryRegistry.getIndexEntryFactoryForContainerSize().createIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, new Long(containerSize));
195                                    _perform(cryptoContext, sizeIdxEntry, dataEntryID);
196                            }
197                            else if (dnMemberMetaData.hasCollection() || dnMemberMetaData.hasArray()) {
198                                    FieldMetaRole role;
199                                    if (dnMemberMetaData.hasCollection()) // Collection.class.isAssignableFrom(dnMemberMetaData.getType()))
200                                            role = FieldMetaRole.collectionElement;
201                                    else
202                                            role = FieldMetaRole.arrayElement;
203    
204                                    FieldMeta subFieldMeta = fieldMeta.getSubFieldMeta(role);
205                                    Object[] fieldValueArray = (Object[]) fieldValue;
206                                    if (fieldValueArray != null) {
207                                            for (Object element : fieldValueArray) {
208                                                    Long otherDataEntryID = ObjectContainerHelper.referenceToDataEntryID(cryptoContext, pmData, element);
209                                                    IndexEntry indexEntry = getIndexEntryForObjectRelation(cryptoContext, pmIndex, subFieldMeta, classMeta, otherDataEntryID);
210                                                    _perform(cryptoContext, indexEntry, dataEntryID);
211                                            }
212                                    }
213    
214                                    // Add entry for the collection/array size
215                                    int containerSize = (fieldValueArray != null ? fieldValueArray.length : 0);
216                                    IndexEntry sizeIdxEntry =
217                                            indexEntryFactoryRegistry.getIndexEntryFactoryForContainerSize().createIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, new Long(containerSize));
218                                    _perform(cryptoContext, sizeIdxEntry, dataEntryID);
219                            }
220                    }
221            }
222    
223            static class Add extends IndexEntryAction
224            {
225                    public Add(Cumulus4jPersistenceHandler persistenceHandler) {
226                            super(persistenceHandler);
227                    }
228    
229                    @Override
230                    public IndexEntry getIndexEntry(IndexEntryFactory indexEntryFactory, CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Object fieldValue)
231                    {
232                            return indexEntryFactory == null ? null : indexEntryFactory.createIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, fieldValue);
233                    }
234    
235                    @Override
236                    public IndexEntry getIndexEntryForObjectRelation(CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Long otherDataEntryID) {
237                            return IndexEntryObjectRelationHelper.createIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, otherDataEntryID);
238                    }
239    
240                    @Override
241                    protected void _perform(CryptoContext cryptoContext, IndexEntry indexEntry, long dataEntryID)
242                    {
243                            if (indexEntry == null)
244                                    return;
245    
246                            IndexValue indexValue = encryptionHandler.decryptIndexEntry(cryptoContext, indexEntry);
247                            indexValue.addDataEntryID(dataEntryID);
248                            encryptionHandler.encryptIndexEntry(cryptoContext, indexEntry, indexValue);
249                            cryptoContext.getPersistenceManagerForIndex().makePersistent(indexEntry); // We do not persist directly when creating anymore, thus we must persist here. This is a no-op if it's already persistent.
250                    }
251            }
252    
253            static class Remove extends IndexEntryAction
254            {
255                    private static final Logger logger = LoggerFactory.getLogger(Remove.class);
256    
257                    public Remove(Cumulus4jPersistenceHandler persistenceHandler) {
258                            super(persistenceHandler);
259                    }
260    
261                    @Override
262                    public IndexEntry getIndexEntry(IndexEntryFactory indexEntryFactory, CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Object fieldValue)
263                    {
264                            return indexEntryFactory == null ? null : indexEntryFactory.getIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, fieldValue);
265                    }
266    
267                    @Override
268                    protected IndexEntry getIndexEntryForObjectRelation(CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Long otherDataEntryID) {
269                            return IndexEntryObjectRelationHelper.getIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, otherDataEntryID);
270                    }
271    
272                    @Override
273                    protected void _perform(CryptoContext cryptoContext, IndexEntry indexEntry, long dataEntryID)
274                    {
275                            if (indexEntry == null)
276                                    return;
277    
278                            IndexValue indexValue = encryptionHandler.decryptIndexEntry(cryptoContext, indexEntry);
279                            indexValue.removeDataEntryID(dataEntryID);
280                            if (indexValue.isDataEntryIDsEmpty()) {
281                                    PersistenceManager pmIndex = cryptoContext.getPersistenceManagerForIndex();
282                                    try {
283                                            pmIndex.deletePersistent(indexEntry);
284                                            // The above line sometimes (but not always) causes the following error:
285    //                                      org.datanucleus.exceptions.NucleusUserException: Transient instances cant be deleted.
286    //                              at org.datanucleus.ObjectManagerImpl.deleteObjectInternal(ObjectManagerImpl.java:2068)
287    //                              at org.datanucleus.ObjectManagerImpl.deleteObjectWork(ObjectManagerImpl.java:2019)
288    //                              at org.datanucleus.ObjectManagerImpl.deleteObject(ObjectManagerImpl.java:1973)
289    //                              at org.datanucleus.api.jdo.JDOPersistenceManager.jdoDeletePersistent(JDOPersistenceManager.java:815)
290    //                              at org.datanucleus.api.jdo.JDOPersistenceManager.deletePersistent(JDOPersistenceManager.java:833)
291    //                              at org.cumulus4j.store.IndexEntryAction$Remove._perform(IndexEntryAction.java:268)
292    //                              at org.cumulus4j.store.IndexEntryAction.perform(IndexEntryAction.java:206)
293    //                              at org.cumulus4j.store.Cumulus4jPersistenceHandler.updateObject(Cumulus4jPersistenceHandler.java:343)
294                                    } catch (JDOUserException x) {
295                                            logger.warn("_perform: " + x, x);
296                                            // If we cannot delete it, we try to store an empty value. That's not nice, but at least a consistent DB state.
297                                            encryptionHandler.encryptIndexEntry(cryptoContext, indexEntry, indexValue);
298                                            // Make sure it is persisted or we get a new exception (because we just tried to delete it).
299                                            pmIndex.makePersistent(indexEntry);
300                                            pmIndex.flush();
301                                            Query query = pmIndex.newQuery(IndexEntry.class);
302                                            query.setFilter("JDOHelper.getObjectId(this) == :indexEntryID");
303                                            query.setUnique(true);
304                                            IndexEntry persistentIndexEntry = (IndexEntry) query.execute(JDOHelper.getObjectId(indexEntry));
305                                            if (persistentIndexEntry == null)
306                                                    throw new IllegalStateException("Newly persisted IndexEntry did not end up in our database! indexEntryID=" + indexEntry.getIndexEntryID());
307                                    }
308    
309                                    // BEGIN workaround - this works, but is not nice.
310    //                              Object indexEntryID = JDOHelper.getObjectId(indexEntry);
311    //                              if (indexEntryID != null) {
312    //                                      PersistenceManager pmIdx = cryptoContext.getPersistenceManagerForIndex();
313    //                                      pmIdx.flush();
314    //                                      Query query = pmIdx.newQuery(IndexEntry.class);
315    //                                      query.setFilter("JDOHelper.getObjectId(this) == :indexEntryID");
316    //                                      query.setUnique(true);
317    //                                      IndexEntry persistentIndexEntry = (IndexEntry) query.execute(indexEntryID);
318    //                                      if (persistentIndexEntry != null)
319    //                                              pmIdx.deletePersistent(persistentIndexEntry);
320    //                              }
321                                    // END workaround
322                            }
323                            else
324                                    encryptionHandler.encryptIndexEntry(cryptoContext, indexEntry, indexValue);
325                    }
326            }
327    }