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 }