package net.notdot.bdbdatastore.server; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.notdot.bdbdatastore.Indexing; import com.google.appengine.entity.Entity; import com.google.appengine.entity.Entity.Property; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import com.sleepycat.je.DatabaseEntry; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.SecondaryDatabase; import com.sleepycat.je.SecondaryMultiKeyCreator; public class CompositeIndexIndexer implements SecondaryMultiKeyCreator { static final Logger logger = LoggerFactory.getLogger(SinglePropertyIndexer.class); private ByteString kind; private boolean hasAncestor; private ByteString[] fields; public CompositeIndexIndexer(Entity.Index idx) { this.kind = idx.getEntityType(); this.hasAncestor = idx.getAncestor(); // Store the list of field names this.fields = new ByteString[idx.getPropertyCount()]; for(int i = 0; i < idx.getPropertyCount(); i++) this.fields[i] = idx.getProperty(i).getName(); } public void createSecondaryKeys(SecondaryDatabase secondary, DatabaseEntry key, DatabaseEntry data, Set<DatabaseEntry> results) throws DatabaseException { Entity.EntityProto entity; try { entity = Indexing.EntityData.parseFrom(data.getData()).getData(); } catch (InvalidProtocolBufferException e) { // TODO: Make this error message more useful somehow. logger.error("Attempted to index invalid entity"); return; } // Check the kind Entity.Path path = entity.getKey().getPath(); ByteString kind = path.getElement(path.getElementCount() - 1).getType(); if(!kind.equals(this.kind)) return; // Get a sorted list of properties List<Entity.Property> properties = new ArrayList<Entity.Property>(entity.getPropertyList()); Collections.sort(properties, PropertyComparator.instance); // Add pseudo-properties properties.add(Entity.Property.newBuilder() .setName(QuerySpec.KEY_PROPERTY) .setValue(EntityKeyComparator.toPropertyValue(entity.getKey())) .build()); // Construct a map of field name to first occurrence Map<ByteString, Integer> fieldMap = new HashMap<ByteString, Integer>(properties.size()); ByteString currentName = null; for(int i = 0; i < properties.size(); i++) { Entity.Property currentProperty = properties.get(i); if(!currentProperty.getName().equals(currentName)) { currentName = currentProperty.getName(); fieldMap.put(currentName, i); } } if(this.hasAncestor) { this.generateAncestorEntries(results, entity, properties, fieldMap); } else { this.generateEntries(results, properties, fieldMap, Indexing.CompositeIndexKey.newBuilder(), 0); } } private void generateEntries(Set<DatabaseEntry> results, List<Property> properties, Map<ByteString, Integer> fieldMap, Indexing.CompositeIndexKey.Builder current, int idx) { ByteString field = this.fields[idx]; if(!fieldMap.containsKey(field)) // Entity does not contain all fields from index. return; int initialOffset = fieldMap.get(field); // Step through the list of properties until we find one // with a different name to the one we're handling. for(int i = initialOffset; i < properties.size(); i++) { Property currentProperty = properties.get(i); if(!currentProperty.getName().equals(field)) break; // Ensure any recursive invocations pick the next element in the list fieldMap.put(field, i + 1); // Recurse Indexing.CompositeIndexKey.Builder newEntry = current.clone().addValue(currentProperty.getValue()); if(idx + 1 == this.fields.length) { results.add(new DatabaseEntry(newEntry.build().toByteArray())); } else { generateEntries(results, properties, fieldMap, newEntry, idx + 1); } } // Restore the original value for this element of the fieldMap fieldMap.put(field, initialOffset); } private void generateAncestorEntries(Set<DatabaseEntry> results, Entity.EntityProto entity, List<Property> properties, Map<ByteString, Integer> fieldMap) { Entity.Path.Builder ancestor = Entity.Path.newBuilder(); List<Entity.Path.Element> elements = entity.getKey().getPath().getElementList(); for(int i = 0; i < elements.size() - 1; i++) { ancestor.addElement(elements.get(i)); Indexing.CompositeIndexKey.Builder newEntry = Indexing.CompositeIndexKey.newBuilder(); newEntry.setAncestor(ancestor.clone()); if(this.fields.length == 0) { // Ancestor-only index results.add(new DatabaseEntry(newEntry.build().toByteArray())); } else { generateEntries(results, properties, fieldMap, newEntry, 0); } } } }