001    package org.cumulus4j.store.datastoreversion.command;
002    
003    import java.util.ArrayList;
004    import java.util.Comparator;
005    import java.util.HashMap;
006    import java.util.List;
007    import java.util.Map;
008    import java.util.Properties;
009    import java.util.Set;
010    
011    import javax.jdo.PersistenceManager;
012    import javax.jdo.Query;
013    
014    import org.cumulus4j.store.Cumulus4jPersistenceHandler;
015    import org.cumulus4j.store.Cumulus4jStoreManager;
016    import org.cumulus4j.store.EncryptionHandler;
017    import org.cumulus4j.store.IndexEntryAction;
018    import org.cumulus4j.store.ProgressInfo;
019    import org.cumulus4j.store.WorkInProgressException;
020    import org.cumulus4j.store.crypto.CryptoContext;
021    import org.cumulus4j.store.datastoreversion.AbstractDatastoreVersionCommand;
022    import org.cumulus4j.store.datastoreversion.CommandApplyParam;
023    import org.cumulus4j.store.model.ClassMeta;
024    import org.cumulus4j.store.model.DataEntry;
025    import org.cumulus4j.store.model.FieldMeta;
026    import org.cumulus4j.store.model.IndexEntry;
027    import org.cumulus4j.store.model.ObjectContainer;
028    import org.datanucleus.ClassLoaderResolver;
029    import org.datanucleus.ExecutionContext;
030    import org.datanucleus.metadata.AbstractMemberMetaData;
031    import org.slf4j.Logger;
032    import org.slf4j.LoggerFactory;
033    
034    /**
035     * Delete all {@link IndexEntry}s from the datastore and then iterate all
036     * {@link DataEntry}s and re-index them.
037     * <p>
038     * TODO This class currently does not yet ensure that different keys are used. Thus,
039     * very likely the entire index uses only one single key. This should be improved
040     * in a future version.
041     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
042     */
043    public class RecreateIndex extends AbstractDatastoreVersionCommand
044    {
045            private static final Logger logger = LoggerFactory.getLogger(RecreateIndex.class);
046    
047            @Override
048            public int getCommandVersion() {
049                    return 1;
050            }
051    
052            @Override
053            public boolean isFinal() {
054                    return false;
055            }
056    
057            @Override
058            public boolean isKeyStoreDependent() {
059                    return true;
060            }
061    
062            private CommandApplyParam commandApplyParam;
063            private Properties workInProgressStateProperties;
064            private CryptoContext cryptoContext;
065            private PersistenceManager pmIndex;
066            private PersistenceManager pmData;
067            private long keyStoreRefID;
068    
069            private Set<Class<? extends IndexEntry>> indexEntryClasses;
070    
071            @Override
072            public void apply(CommandApplyParam commandApplyParam) {
073                    // The index only exists in the index-datastore (not in the index-datastore), hence we return immediately, if the
074                    // current datastore is not the index-datastore.
075                    this.commandApplyParam = commandApplyParam;
076                    PersistenceManager pm = commandApplyParam.getPersistenceManager();
077                    cryptoContext = commandApplyParam.getCryptoContext();
078                    if (pm != cryptoContext.getPersistenceManagerForIndex())
079                            return;
080    
081                    keyStoreRefID = cryptoContext.getKeyStoreRefID();
082                    pmIndex = commandApplyParam.getCryptoContext().getPersistenceManagerForIndex();
083                    pmData = commandApplyParam.getCryptoContext().getPersistenceManagerForData();
084                    workInProgressStateProperties = commandApplyParam.getWorkInProgressStateProperties();
085    
086                    deleteIndex();
087                    createIndex();
088            }
089    
090            protected static final String PROPERTY_DELETE_COMPLETE = "delete.complete";
091            protected static final String PROPERTY_DELETE_FROM_INDEX_ENTRY_ID = "delete.fromIndexEntryID";
092            protected static final String PROPERTY_CREATE_FROM_DATA_ENTRY_ID = "create.fromDataEntryID";
093    
094            protected void deleteIndex() {
095                    logger.debug("deleteIndex: Entered.");
096                    if (Boolean.parseBoolean(workInProgressStateProperties.getProperty(PROPERTY_DELETE_COMPLETE))) {
097                            logger.debug("deleteIndex: PROPERTY_DELETE_COMPLETE == true => quit.");
098                            return;
099                    }
100    
101                    final long indexEntryBlockSize = 100;
102                    Long maxIndexEntryIDObj = getMaxIndexEntryID();
103                    if (maxIndexEntryIDObj == null) {
104                            logger.debug("deleteIndex: There are no IndexEntry instances in the database => quit.");
105                    }
106                    else {
107                            final long maxIndexEntryID = maxIndexEntryIDObj;
108                            String fromIndexEntryStr = workInProgressStateProperties.getProperty(PROPERTY_DELETE_FROM_INDEX_ENTRY_ID);
109                            long fromIndexEntryID;
110                            if (fromIndexEntryStr != null) {
111                                    logger.info("deleteIndex: previous incomplete run found: fromIndexEntryStr={}", fromIndexEntryStr);
112                                    fromIndexEntryID = Long.parseLong(fromIndexEntryStr);
113                            }
114                            else {
115                                    final long minIndexEntryID = getMinIndexEntryID();
116                                    logger.info("deleteIndex: first run: minIndexEntryID={} maxIndexEntryID={}", minIndexEntryID, maxIndexEntryID);
117                                    fromIndexEntryID = minIndexEntryID;
118                            }
119                            while (fromIndexEntryID <= maxIndexEntryID - indexEntryBlockSize) {
120                                    long toIndexEntryIDExcl = fromIndexEntryID + indexEntryBlockSize;
121                                    deleteIndexForRange(fromIndexEntryID, toIndexEntryIDExcl);
122                                    fromIndexEntryID = toIndexEntryIDExcl;
123                                    if (commandApplyParam.isDatastoreVersionCommandApplyWorkInProgressTimeoutExceeded()) {
124                                            workInProgressStateProperties.setProperty(PROPERTY_DELETE_FROM_INDEX_ENTRY_ID, Long.toString(fromIndexEntryID));
125                                            throw new WorkInProgressException(new ProgressInfo());
126                                    }
127                            }
128                            deleteIndexForRange(fromIndexEntryID, null);
129                    }
130    
131                    workInProgressStateProperties.setProperty(PROPERTY_DELETE_COMPLETE, Boolean.TRUE.toString());
132                    logger.debug("deleteIndex: Leaving.");
133            }
134    
135            protected void deleteIndexForRange(long fromIndexEntryIDIncl, Long toIndexEntryIDExcl) {
136                    logger.info("deleteIndexForRange: Entered. fromIndexEntryIDIncl={} toIndexEntryIDExcl={}", fromIndexEntryIDIncl, toIndexEntryIDExcl);
137                    List<IndexEntry> indexEntries = getIndexEntries(fromIndexEntryIDIncl, toIndexEntryIDExcl);
138                    pmIndex.deletePersistentAll(indexEntries);
139                    logger.info("deleteIndexForRange: Leaving. fromIndexEntryIDIncl={} toIndexEntryIDExcl={}", fromIndexEntryIDIncl, toIndexEntryIDExcl);
140            }
141    
142            protected List<IndexEntry> getIndexEntries(long fromIndexEntryIDIncl, Long toIndexEntryIDExcl) {
143                    List<IndexEntry> result = new ArrayList<IndexEntry>();
144                    for (Class<? extends IndexEntry> indexEntryClass : getIndexEntryClasses()) {
145                            result.addAll(getIndexEntries(indexEntryClass, fromIndexEntryIDIncl, toIndexEntryIDExcl));
146                    }
147                    return result;
148            }
149    
150            protected List<IndexEntry> getIndexEntries(Class<? extends IndexEntry> indexEntryClass, long fromIndexEntryIDIncl, Long toIndexEntryIDExcl) {
151                    Query q = pmIndex.newQuery(indexEntryClass);
152                    StringBuilder filter = new StringBuilder();
153                    Map<String, Object> params = new HashMap<String, Object>(2);
154    
155                    filter.append("this.keyStoreRefID == :keyStoreRefID");
156                    params.put("keyStoreRefID", keyStoreRefID);
157    
158                    if (fromIndexEntryIDIncl > 0) { // required for GAE, because it throws an exception when querying for ID >= 0, saying that ID == 0 is illegal.
159                            filter.append(" && this.indexEntryID >= :fromIndexEntryIDIncl");
160                            params.put("fromIndexEntryIDIncl", fromIndexEntryIDIncl);
161                    }
162    
163                    if (toIndexEntryIDExcl != null) {
164                            filter.append(" && this.indexEntryID < :toIndexEntryIDExcl");
165                            params.put("toIndexEntryIDExcl", toIndexEntryIDExcl);
166                    }
167                    q.setFilter(filter.toString());
168                    q.setOrdering("this.indexEntryID ASC");
169    
170                    @SuppressWarnings("unchecked")
171                    List<IndexEntry> result = new ArrayList<IndexEntry>((List<IndexEntry>) q.executeWithMap(params));
172                    q.closeAll();
173                    return result;
174            }
175    
176            protected void createIndex()
177            {
178                    final long dataEntryBlockSize = 100;
179                    long fromDataEntryID;
180                    String fromDataEntryIDStr = workInProgressStateProperties.getProperty(PROPERTY_CREATE_FROM_DATA_ENTRY_ID);
181                    final long maxDataEntryID = getMaxDataEntryID();
182                    if (fromDataEntryIDStr != null) {
183                            fromDataEntryID = Long.parseLong(fromDataEntryIDStr);
184                    }
185                    else {
186                            final long minDataEntryID = getMinDataEntryID();
187                            fromDataEntryID = minDataEntryID;
188                    }
189                    while (fromDataEntryID <= maxDataEntryID - dataEntryBlockSize) {
190                            long toDataEntryIDExcl = fromDataEntryID + dataEntryBlockSize;
191                            createIndexForRange(fromDataEntryID, toDataEntryIDExcl);
192                            fromDataEntryID = toDataEntryIDExcl;
193    
194                            if (commandApplyParam.isDatastoreVersionCommandApplyWorkInProgressTimeoutExceeded()) {
195                                    workInProgressStateProperties.setProperty(PROPERTY_CREATE_FROM_DATA_ENTRY_ID, Long.toString(fromDataEntryID));
196                                    throw new WorkInProgressException(new ProgressInfo());
197                            }
198                    }
199                    createIndexForRange(fromDataEntryID, null);
200            }
201    
202            protected void createIndexForRange(long fromDataEntryIDIncl, Long toDataEntryIDExcl) {
203                    ExecutionContext ec = cryptoContext.getExecutionContext();
204                    ClassLoaderResolver clr = ec.getClassLoaderResolver();
205                    Cumulus4jStoreManager storeManager = commandApplyParam.getStoreManager();
206                    EncryptionHandler encryptionHandler = storeManager.getEncryptionHandler();
207                    Cumulus4jPersistenceHandler persistenceHandler = storeManager.getPersistenceHandler();
208                    IndexEntryAction addIndexEntryAction = persistenceHandler.getAddIndexEntryAction();
209                    List<DataEntryWithClassName> l = getDataEntries(fromDataEntryIDIncl, toDataEntryIDExcl);
210                    for (DataEntryWithClassName dataEntryWithClassName : l) {
211                            long dataEntryID = dataEntryWithClassName.getDataEntry().getDataEntryID();
212                            Class<?> clazz = clr.classForName(dataEntryWithClassName.getClassName());
213                            ClassMeta classMeta = storeManager.getClassMeta(ec, clazz);
214                            ObjectContainer objectContainer = encryptionHandler.decryptDataEntry(cryptoContext, dataEntryWithClassName.getDataEntry());
215                            for (Map.Entry<Long, Object> me : objectContainer.getFieldID2value().entrySet()) {
216                                    long fieldID = me.getKey();
217                                    Object fieldValue = me.getValue();
218                                    FieldMeta fieldMeta = classMeta.getFieldMeta(fieldID);
219                                    AbstractMemberMetaData dnMemberMetaData = fieldMeta.getDataNucleusMemberMetaData(ec);
220                                    addIndexEntryAction.perform(cryptoContext, dataEntryID, fieldMeta, dnMemberMetaData, classMeta, fieldValue);
221                            }
222                    }
223            }
224    
225            protected static final class DataEntryWithClassName {
226                    private DataEntry dataEntry;
227                    private String className;
228    
229                    public DataEntryWithClassName(DataEntry dataEntry, String className) {
230                            if (dataEntry == null)
231                                    throw new IllegalArgumentException("dataEntry == null");
232                            if (className == null)
233                                    throw new IllegalArgumentException("className == null");
234                            this.dataEntry = dataEntry;
235                            this.className = className;
236                    }
237                    /**
238                     * Get the {@link DataEntry}.
239                     * @return the {@link DataEntry}. Never <code>null</code>.
240                     */
241                    public DataEntry getDataEntry() {
242                            return dataEntry;
243                    }
244                    /**
245                     * Get the fully qualified class name of the persistence-capable object represented by
246                     * {@link #dataEntry}.
247                     * @return the fully qualified class name. Never <code>null</code>.
248                     */
249                    public String getClassName() {
250                            return className;
251                    }
252            }
253    
254            protected List<DataEntryWithClassName> getDataEntries(long fromDataEntryIDIncl, Long toDataEntryIDExcl) {
255                    Query q = pmData.newQuery(DataEntry.class);
256                    q.declareVariables(ClassMeta.class.getName() + " classMeta");
257                    q.setResult("this, classMeta.packageName, classMeta.simpleClassName");
258                    StringBuilder filter = new StringBuilder();
259                    Map<String, Object> params = new HashMap<String, Object>(2);
260    
261                    filter.append("this.classMeta_classID == classMeta.classID");
262                    filter.append(" && this.keyStoreRefID == :keyStoreRefID");
263                    params.put("keyStoreRefID", keyStoreRefID);
264    
265                    if (fromDataEntryIDIncl > 0) { // required for GAE, because it throws an exception when querying for ID >= 0, saying that ID == 0 is illegal.
266                            filter.append(" && this.dataEntryID >= :fromDataEntryIDIncl");
267                            params.put("fromDataEntryIDIncl", fromDataEntryIDIncl);
268                    }
269    
270                    if (toDataEntryIDExcl != null) {
271                            filter.append(" && this.dataEntryID < :toDataEntryIDExcl");
272                            params.put("toDataEntryIDExcl", toDataEntryIDExcl);
273                    }
274                    q.setFilter(filter.toString());
275                    q.setOrdering("this.dataEntryID ASC");
276    
277                    @SuppressWarnings("unchecked")
278                    List<Object[]> l = (List<Object[]>) q.executeWithMap(params);
279                    List<DataEntryWithClassName> result = new ArrayList<DataEntryWithClassName>(l.size());
280                    for (Object[] row : l) {
281                            if (row.length != 3)
282                                    throw new IllegalStateException(String.format("row.length == %s != 3", row.length));
283    
284                            result.add(new DataEntryWithClassName(
285                                            (DataEntry)row[0],
286                                            ClassMeta.getClassName((String)row[1], (String)row[2])
287                            ));
288                    }
289                    q.closeAll();
290                    return result;
291            }
292    
293            protected long getMinDataEntryID() {
294                    Long result = getMinMaxDataEntryID("min");
295                    if (result == null)
296                            return 0;
297                    return result;
298            }
299    
300            protected long getMaxDataEntryID() {
301                    Long result = getMinMaxDataEntryID("max");
302                    if (result == null)
303                            return 0;
304                    return result;
305            }
306    
307            protected Long getMinMaxDataEntryID(String minMax) {
308                    Query q = pmData.newQuery(DataEntry.class);
309                    q.setResult(minMax + "(this.dataEntryID)");
310                    Long result = (Long) q.execute();
311                    return result;
312            }
313    
314            protected Long getMinIndexEntryID() {
315                    return getMinMaxIndexEntryID("min", new Comparator<Long>() {
316                            @Override
317                            public int compare(Long o1, Long o2) {
318                                    return o2.compareTo(o1);
319                            }
320                    });
321            }
322    
323            protected Long getMaxIndexEntryID() {
324                    return getMinMaxIndexEntryID("max", new Comparator<Long>() {
325                            @Override
326                            public int compare(Long o1, Long o2) {
327                                    return o1.compareTo(o2);
328                            }
329                    });
330            }
331    
332            protected Long getMinMaxIndexEntryID(String minMax, Comparator<Long> comparator) {
333                    Long result = null;
334                    for (Class<? extends IndexEntry> indexEntryClass : getIndexEntryClasses()) {
335                            Long minMaxIndexEntryID = getMinMaxIndexEntryID(indexEntryClass, minMax);
336                            if (minMaxIndexEntryID != null) {
337                                    if (result == null || comparator.compare(result, minMaxIndexEntryID) < 0)
338                                            result = minMaxIndexEntryID;
339                            }
340                    }
341                    return result;
342            }
343    
344            protected Long getMinMaxIndexEntryID(Class<? extends IndexEntry> indexEntryClass, String minMax) {
345                    Query q = pmIndex.newQuery(indexEntryClass);
346                    q.setResult(minMax + "(this.indexEntryID)");
347                    Long result = (Long) q.execute();
348                    return result;
349            }
350    
351            protected Set<Class<? extends IndexEntry>> getIndexEntryClasses() {
352                    if (indexEntryClasses == null)
353                            indexEntryClasses = ((Cumulus4jStoreManager)cryptoContext.getExecutionContext().getStoreManager()).getIndexFactoryRegistry().getIndexEntryClasses();
354    
355                    return indexEntryClasses;
356            }
357    }