package mil.nga.giat.geowave.adapter.vector.plugin.transaction; import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import mil.nga.giat.geowave.adapter.vector.plugin.GeoWaveDataStoreComponents; import mil.nga.giat.geowave.adapter.vector.plugin.lock.LockingManagement; import mil.nga.giat.geowave.core.index.ByteArrayId; import mil.nga.giat.geowave.core.store.CloseableIterator; import mil.nga.giat.geowave.core.store.adapter.statistics.DataStatistics; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.geotools.data.Transaction; import org.geotools.factory.Hints; import org.geotools.geometry.jts.ReferencedEnvelope; import org.opengis.feature.simple.SimpleFeature; import org.opengis.filter.Filter; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; /** * Captures changes made to a FeatureStore prior to being committed. * <p> * This is used to simulate the functionality of a database including * transaction independence. * * * @source $URL$ */ public class GeoWaveTransactionManagement extends AbstractTransactionManagement implements GeoWaveTransaction { protected static final Logger LOGGER = LoggerFactory.getLogger(GeoWaveTransactionManagement.class); /** Map of modified features; by feature id */ private final Map<String, ModifiedFeature> modifiedFeatures = new ConcurrentHashMap<String, ModifiedFeature>(); private final Map<String, SimpleFeature> addedFeatures = new ConcurrentHashMap<String, SimpleFeature>(); private final Multimap<String, SimpleFeature> removedFeatures = LinkedListMultimap.create(); private Map<ByteArrayId, DataStatistics<SimpleFeature>> statsCache = null; /** List of added feature ids; values stored in added above */ private final Set<String> addedFidList = new HashSet<String>(); private int maxAdditionBufferSize = 10000; private final LockingManagement lockingManager; private final Transaction transaction; private final String txID; private final String typeName; private static class ModifiedFeature { public ModifiedFeature( final SimpleFeature oldFeature, final SimpleFeature newFeature, final boolean alreadyWritten ) { super(); this.newFeature = newFeature; this.oldFeature = oldFeature; this.alreadyWritten = alreadyWritten; } final boolean alreadyWritten; final SimpleFeature newFeature; final SimpleFeature oldFeature; } /** Simple object used for locking */ final Object mutex; /** * Create an empty Diff * * @throws IOException */ public GeoWaveTransactionManagement( final int maxAdditionBufferSize, final GeoWaveDataStoreComponents components, final String typeName, final Transaction transaction, final LockingManagement lockingManager, final String txID ) throws IOException { super( components); this.maxAdditionBufferSize = maxAdditionBufferSize; mutex = this; this.typeName = typeName; this.transaction = transaction; this.lockingManager = lockingManager; this.txID = txID; } /** * Check if modifiedFeatures and addedFeatures are empty. * * @return true if Diff is empty */ @Override public boolean isEmpty() { synchronized (mutex) { return modifiedFeatures.isEmpty() && addedFidList.isEmpty() && removedFeatures.isEmpty() && addedFeatures.isEmpty(); } } /** * Clear diff - called during rollback. */ public void clear() { synchronized (mutex) { addedFidList.clear(); modifiedFeatures.clear(); removedFeatures.clear(); addedFeatures.clear(); } } /** * Record a modification to the indicated fid * * @param fid * @param f * replacement feature; null to indicate remove */ @Override public void modify( final String fid, final SimpleFeature original, final SimpleFeature updated ) throws IOException { lockingManager.lock( transaction, fid); // assumptions: (1) will not get a modification to a deleted feature // thus, only contents of the removed features collection for this // feature relate to moving bounds. // @see {@link #interweaveTransaction(CloseableIterator<SimpleFeature>)} // // Cannot assume that a modification occurs for a newly added fid // TODO: skipping this for now. creates a problem because // the row IDs may or maynot change. If they change completely, then // it is not an issue. However, a mix of changed or unchanged means // that the original rows become invisible for the duration of the // transaction // The problem now is that the bounded query may not return the moved // record, if it has moved outside // the query space. oh well! final ModifiedFeature modRecord = modifiedFeatures.get(fid); if (!updated.getBounds().equals( original.getBounds())) { // retain original--original position is removed later. // The original feature needs to be excluded in a query // and removed at commit removedFeatures.put( fid, original); } if (((modRecord != null) && modRecord.alreadyWritten) || addedFidList.contains(fid)) { components.writeCommit( updated, this); synchronized (mutex) { if (modRecord != null) { modifiedFeatures.put( fid, new ModifiedFeature( modRecord.oldFeature, updated, true)); } else { LOGGER.error("modRecord was set to null in another thread; synchronization issue"); } } } else { synchronized (mutex) { modifiedFeatures.put( fid, new ModifiedFeature( modRecord == null ? original : modRecord.oldFeature, updated, false)); } } final ReferencedEnvelope bounds = new ReferencedEnvelope( (CoordinateReferenceSystem) null); bounds.include(original.getBounds()); bounds.include(updated.getBounds()); components.getGTstore().getListenerManager().fireFeaturesChanged( components.getAdapter().getFeatureType().getTypeName(), transaction, bounds, false); } @Override public void add( final String fid, final SimpleFeature feature ) throws IOException { feature.getUserData().put( Hints.USE_PROVIDED_FID, true); if (feature.getUserData().containsKey( Hints.PROVIDED_FID)) { final String providedFid = (String) feature.getUserData().get( Hints.PROVIDED_FID); feature.getUserData().put( Hints.PROVIDED_FID, providedFid); } else { feature.getUserData().put( Hints.PROVIDED_FID, feature.getID()); } if (addedFeatures.size() >= maxAdditionBufferSize) { flushAddsToStore(true); } addedFeatures.put( fid, feature); components.getGTstore().getListenerManager().fireFeaturesAdded( components.getAdapter().getFeatureType().getTypeName(), transaction, ReferencedEnvelope.reference(feature.getBounds()), false); } @Override public void remove( final String fid, final SimpleFeature feature ) throws IOException { synchronized (mutex) { if (addedFidList.remove(fid)) { components.remove( feature, this); } else { addedFeatures.remove(fid); // will remove at the end of the transaction, except ones // created in the transaction. removedFeatures.put( fid, feature); modifiedFeatures.remove(fid); } } components.getGTstore().getListenerManager().fireFeaturesRemoved( components.getAdapter().getFeatureType().getTypeName(), transaction, ReferencedEnvelope.reference(feature.getBounds()), false); } public void rollback() throws IOException { statsCache = null; for (final String fid : addedFidList) { components.remove( fid, this); } clear(); } @Override public String[] composeAuthorizations() { return components.getGTstore().getAuthorizationSPI().getAuthorizations(); } @Override public String composeVisibility() { return txID; } public String getID() { return txID; } @Override public void flush() throws IOException { flushAddsToStore(true); } private void flushAddsToStore( final boolean autoCommitAdds ) throws IOException { final Set<String> captureList = autoCommitAdds ? new HashSet<String>() : addedFidList; components.write( addedFeatures.values().iterator(), captureList, autoCommitAdds ? new GeoWaveEmptyTransaction( components) : this); addedFeatures.clear(); } public void commit() throws IOException { flushAddsToStore(true); final Iterator<Pair<SimpleFeature, SimpleFeature>> updateIt = getUpdates(); // if (addedFidList.size() > 0) { // final String transId = "\\(?" + txID + "\\)?"; // final VisibilityTransformer visibilityTransformer = new // VisibilityTransformer( // "&?" + transId, // ""); // for (final Collection<ByteArrayId> rowIDs : addedFidList.values()) { // components.replaceDataVisibility( // this, // rowIDs, // visibilityTransformer); // } // // components.replaceStatsVisibility( // this, // visibilityTransformer); // } final Iterator<SimpleFeature> removeIt = removedFeatures.values().iterator(); while (removeIt.hasNext()) { final SimpleFeature delFeatured = removeIt.next(); components.remove( delFeatured, this); final ModifiedFeature modFeature = modifiedFeatures.get(delFeatured.getID()); // only want notify updates to existing (not new) features if ((modFeature == null) || modFeature.alreadyWritten) { components.getGTstore().getListenerManager().fireFeaturesRemoved( typeName, transaction, ReferencedEnvelope.reference(delFeatured.getBounds()), true); } } while (updateIt.hasNext()) { final Pair<SimpleFeature, SimpleFeature> pair = updateIt.next(); components.writeCommit( pair.getRight(), new GeoWaveEmptyTransaction( components)); final ReferencedEnvelope bounds = new ReferencedEnvelope( (CoordinateReferenceSystem) null); bounds.include(pair.getLeft().getBounds()); bounds.include(pair.getRight().getBounds()); components.getGTstore().getListenerManager().fireFeaturesChanged( typeName, transaction, ReferencedEnvelope.reference(pair.getRight().getBounds()), true); } statsCache = null; } private Iterator<Pair<SimpleFeature, SimpleFeature>> getUpdates() { final Iterator<Entry<String, ModifiedFeature>> entries = modifiedFeatures.entrySet().iterator(); return new Iterator<Pair<SimpleFeature, SimpleFeature>>() { Pair<SimpleFeature, SimpleFeature> pair = null; @Override public boolean hasNext() { while (entries.hasNext() && (pair == null)) { final Entry<String, ModifiedFeature> entry = entries.next(); if (!entry.getValue().alreadyWritten) { pair = Pair.of( entry.getValue().oldFeature, entry.getValue().newFeature); } else { pair = null; } } return pair != null; } @Override public Pair<SimpleFeature, SimpleFeature> next() throws NoSuchElementException { if (pair == null) { throw new NoSuchElementException(); } final Pair<SimpleFeature, SimpleFeature> retVal = pair; pair = null; return retVal; } @Override public void remove() {} }; } @Override public Map<ByteArrayId, DataStatistics<SimpleFeature>> getDataStatistics() { if (statsCache == null) { statsCache = super.getDataStatistics(); } return statsCache; } @Override public CloseableIterator<SimpleFeature> interweaveTransaction( final Integer limit, final Filter filter, final CloseableIterator<SimpleFeature> it ) { return new CloseableIterator<SimpleFeature>() { Iterator<SimpleFeature> addedIt = addedFeatures.values().iterator(); SimpleFeature feature = null; long count = 0; @Override public boolean hasNext() { if (limit != null && limit.intValue() > 0 && count > limit) return false; while (addedIt.hasNext() && (feature == null)) { feature = addedIt.next(); if (!filter.evaluate(feature)) feature = null; } while (it.hasNext() && (feature == null)) { feature = it.next(); final ModifiedFeature modRecord = modifiedFeatures.get(feature.getID()); // exclude removed features // and include updated features not written yet. final Collection<SimpleFeature> oldFeatures = removedFeatures.get(feature.getID()); if (modRecord != null) { feature = modRecord.newFeature; } else if ((oldFeatures != null) && !oldFeatures.isEmpty()) { // need to check if the removed feature // was just moved meaning its original matches the // boundaries of this 'feature'. matchesOne(oldFeatures, // feature)) feature = null; } } return feature != null; } @Override public SimpleFeature next() throws NoSuchElementException { if (feature == null) { throw new NoSuchElementException(); } final SimpleFeature retVal = feature; feature = null; count++; return retVal; } @Override public void remove() { removedFeatures.put( feature.getID(), feature); } @Override public void close() throws IOException { it.close(); } }; } }