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.model;
019    
020    import java.util.ArrayList;
021    import java.util.Collections;
022    import java.util.HashMap;
023    import java.util.List;
024    import java.util.Map;
025    
026    import javax.jdo.PersistenceManager;
027    
028    import org.cumulus4j.store.Cumulus4jStoreManager;
029    import org.cumulus4j.store.EncryptionHandler;
030    import org.cumulus4j.store.crypto.CryptoContext;
031    import org.datanucleus.store.ExecutionContext;
032    
033    /**
034     * <p>
035     * Factory for creating (or looking up) specific {@link IndexEntry} implementations.
036     * </p><p>
037     * It is optional to implement a specific factory. For most use cases, it is sufficient to
038     * use the {@link DefaultIndexEntryFactory} (which is used, if the extension specifies the
039     * attribute <code>index-entry-type</code>), but you can alternatively specify a custom
040     * factory via the extension-attribute <code>index-entry-factory-type</code>.
041     * </p><p>
042     * If you specify a custom
043     * factory, you must omit (or leave empty) the <code>index-entry-type</code>!
044     * </p>
045     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
046     */
047    public abstract class IndexEntryFactory
048    {
049            /**
050             * Get the concrete implementation class (sub-class) of {@link IndexEntry} managed by this factory.
051             * @return the concrete implementation class of {@link IndexEntry} managed by this factory.
052             */
053            public abstract Class<? extends IndexEntry> getIndexEntryClass();
054    
055            public List<IndexEntry> getIndexEntriesIncludingSubClasses(CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Object indexKey)
056            {
057                    ExecutionContext ec = cryptoContext.getExecutionContext();
058                    Cumulus4jStoreManager storeManager = (Cumulus4jStoreManager) ec.getStoreManager();
059                    List<ClassMeta> classMetaWithSubClassMetas = storeManager.getClassMetaWithSubClassMetas(ec, classMeta);
060                    return getIndexEntries(cryptoContext, pmIndex, fieldMeta, classMetaWithSubClassMetas, indexKey);
061            }
062    
063            public List<IndexEntry> getIndexEntries(CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, List<ClassMeta> classMetas, Object indexKey)
064            {
065                    if (pmIndex == null)
066                            throw new IllegalArgumentException("pm == null");
067    
068                    if (fieldMeta == null)
069                            throw new IllegalArgumentException("fieldMeta == null");
070    
071                    if (classMetas == null)
072                            throw new IllegalArgumentException("classMetas == null");
073    
074                    if (classMetas.isEmpty()) {
075                            throw new IllegalArgumentException("classMetas is empty"); // hmmm... I think this should never happen.
076    //                      return Collections.emptyList();
077                    }
078    
079                    if (classMetas.size() == 1) {
080                            IndexEntry indexEntry = getIndexEntry(cryptoContext, pmIndex, fieldMeta, classMetas.get(0), indexKey);
081                            if (indexEntry == null)
082                                    return Collections.emptyList();
083                            else
084                                    return Collections.singletonList(indexEntry);
085                    }
086    //              List<IndexEntry> result = new ArrayList<IndexEntry>(classMetas.size());
087    //              for (ClassMeta classMeta : classMetas) {
088    //                      IndexEntry indexEntry = getIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, indexKey);
089    //                      if (indexEntry != null)
090    //                              result.add(indexEntry);
091    //              }
092    //              return result;
093    
094                    Class<? extends IndexEntry> indexEntryClass = getIndexEntryClass();
095                    javax.jdo.Query q = pmIndex.newQuery(indexEntryClass);
096                    Map<String, Object> params = new HashMap<String, Object>();
097                    q.setFilter(
098                                    "this.keyStoreRefID == :keyStoreRefID && " +
099                                    "this.fieldMeta_fieldID == :fieldMeta_fieldID && " +
100    //                              ":classMetas.contains(this.classMeta) && " +
101                                    ClassMetaDAO.getMultiClassMetaOrFilterPart(params, classMetas) + " && " +
102                                    "this.indexKey == :indexKey"
103                    );
104                    params.put("keyStoreRefID", cryptoContext.getKeyStoreRefID());
105                    params.put("fieldMeta_fieldID", fieldMeta.getFieldID());
106    //              params.put("classMetas", classMetas);
107                    params.put("indexKey", indexKey);
108                    @SuppressWarnings("unchecked")
109                    List<IndexEntry> result = (List<IndexEntry>) q.executeWithMap(params);
110                    result = Collections.unmodifiableList(new ArrayList<IndexEntry>(result)); // consistent with emptyList + singletonList above (both read-only)
111                    q.closeAll();
112                    return result;
113            }
114    
115            /**
116             * Get an {@link IndexEntry} for the specified unique key fields or <code>null</code>, if no such instance
117             * exists.
118             * @param cryptoContext the crypto-context.
119             * @param pmIndex the backend-<code>PersistenceManager</code>. Must not be <code>null</code>.
120             * @param fieldMeta the meta-data of the field to query. Must not be <code>null</code>.
121             * @param classMeta TODO
122             * @param indexKey the indexed value to search for. Might be <code>null</code> (<code>null</code> can be indexed).
123             * @return the matching {@link IndexEntry} or <code>null</code>.
124             */
125            public IndexEntry getIndexEntry(CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Object indexKey)
126            {
127                    if (pmIndex == null)
128                            throw new IllegalArgumentException("pm == null");
129    
130                    if (fieldMeta == null)
131                            throw new IllegalArgumentException("fieldMeta == null");
132    
133                    if (classMeta == null)
134                            throw new IllegalArgumentException("classMeta == null");
135    
136                    Class<? extends IndexEntry> indexEntryClass = getIndexEntryClass();
137                    javax.jdo.Query q = pmIndex.newQuery(indexEntryClass);
138                    q.setUnique(true);
139                    q.setFilter(
140                                    "this.keyStoreRefID == :keyStoreRefID && " +
141                                    "this.fieldMeta_fieldID == :fieldMeta_fieldID && " +
142                                    "this.classMeta_classID == :classMeta_classID && " +
143                                    "this.indexKey == :indexKey"
144                    );
145                    Map<String, Object> params = new HashMap<String, Object>();
146                    params.put("keyStoreRefID", cryptoContext.getKeyStoreRefID());
147                    params.put("fieldMeta_fieldID", fieldMeta.getFieldID());
148                    params.put("classMeta_classID", classMeta.getClassID());
149                    params.put("indexKey", indexKey);
150                    return indexEntryClass.cast(q.executeWithMap(params));
151            }
152    
153            /**
154             * Get an existing {@link IndexEntry} just like {@link #getIndexEntry(CryptoContext, PersistenceManager, FieldMeta, ClassMeta, Object)}
155             * or create one, if it does not yet exist.
156             * @param cryptoContext TODO
157             * @param pmIndex the backend-<code>PersistenceManager</code>. Must not be <code>null</code>.
158             * @param fieldMeta the meta-data of the field to query. Must not be <code>null</code>.
159             * @param classMeta TODO
160             * @param indexKey the indexed value to search for. Might be <code>null</code> (<code>null</code> can be indexed).
161             * @return the matching {@link IndexEntry} (never <code>null</code>).
162             */
163            public IndexEntry createIndexEntry(CryptoContext cryptoContext, PersistenceManager pmIndex, FieldMeta fieldMeta, ClassMeta classMeta, Object indexKey)
164            {
165                    IndexEntry result = getIndexEntry(cryptoContext, pmIndex, fieldMeta, classMeta, indexKey);
166                    if (result == null) {
167                            try {
168                                    result = getIndexEntryClass().newInstance();
169                            } catch (InstantiationException e) {
170                                    throw new RuntimeException(e);
171                            } catch (IllegalAccessException e) {
172                                    throw new RuntimeException(e);
173                            }
174                            result.setFieldMeta(fieldMeta);
175                            result.setClassMeta(classMeta);
176                            result.setKeyStoreRefID(cryptoContext.getKeyStoreRefID());
177                            result.setIndexKey(indexKey);
178    
179                            // We persist *after* setting all values, because that improves performance:
180                            // This way, there is only one INSERT instead of one INSERT AND one UPDATE for each new
181                            // index entry. The MovieQueryTest.importDataCsv() is around 10% faster when using MySQL
182                            // (approximately 60 sec vs. 66 sec).
183                            // However, when dumping the plaintexts for debugging, we need the indexEntryID already *before*
184                            // encryption. Hence, we persist here, if the DEBUG_DUMP flag is set.
185                            // Marco :-)
186                            if (EncryptionHandler.DEBUG_DUMP)
187                                    result = pmIndex.makePersistent(result);
188                    }
189    
190                    return result;
191            }
192    }