Java tutorial
/* * polymap.org * Copyright 2011, Polymap GmbH. All rights reserved. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.polymap.core.data.feature.buffer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.io.IOException; import net.refractions.udig.catalog.IGeoResource; import org.geotools.data.DefaultTransaction; import org.geotools.data.FeatureStore; import org.geotools.data.Transaction; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureCollections; import org.opengis.feature.Feature; import org.opengis.feature.Property; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.Id; import org.opengis.filter.identity.FeatureId; import org.opengis.filter.identity.Identifier; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.qi4j.api.unitofwork.NoSuchEntityException; import com.vividsolutions.jts.geom.Geometry; import org.eclipse.core.runtime.IProgressMonitor; import org.polymap.core.data.DataPlugin; import org.polymap.core.data.FeatureChangeEvent; import org.polymap.core.data.FeatureChangeEvent.Type; import org.polymap.core.data.FeatureStateTracker; import org.polymap.core.data.feature.DataSourceProcessor; import org.polymap.core.data.feature.FidSet; import org.polymap.core.operation.IOperationSaveListener; import org.polymap.core.operation.OperationSupport; import org.polymap.core.project.ILayer; import org.polymap.core.runtime.Polymap; import org.polymap.core.runtime.SessionSingleton; import org.polymap.core.runtime.entity.ConcurrentModificationException; import org.polymap.core.runtime.entity.EntityHandle; import org.polymap.core.runtime.entity.EntityStateTracker; import org.polymap.core.runtime.entity.EntityStateTracker.Updater; import org.polymap.core.runtime.event.EventManager; import org.polymap.core.workbench.PolymapWorkbench; /** * The API and mediator of the feature buffer system. * <p/> * There is one buffer per layer per session. The buffer content is injected into the * feature pipeline by the {@link FeatureBufferProcessor}. The processor is installed * by the {@link InstallBufferPipelineListener}. * <p/> * The buffer manager handles save/revert events by writing down the buffer content * to the underlying data store. <b>Concurrent changes</b> are checked against the * original copy of the features that the buffer is providing via * {@link FeatureBufferState}. This strategy might be memory consuming but it is also * a robust way to check concurrent changes that does not depend on a timestamp in * the data type. * <p/> * The buffer manager writes down the changes to the underlying FeatureStore directly * without using the pipeline of the layer. So the buffer processor MUST be the first * after the {@link DataSourceProcessor}. * <p/> * XXX Currently the original state of an modified feature is requested from the * underlying store not before the modification request arrives. This may lead to * <b>lost updates</b> if the feature has changed meanwhile. In order to detect * <b>every</b> modification the {@link FeatureBufferProcessor} presets the timestamp * in the feature and {@link FeatureBufferState#timestamp()} recognizes this. * {@link FeatureStateTracker} should be able to detect concurrent modufication now. * Not fully tested. * * @author <a href="http://www.polymap.de">Falko Brutigam</a> */ public class LayerFeatureBufferManager implements IOperationSaveListener { private static Log log = LogFactory.getLog(LayerFeatureBufferManager.class); public static final FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); /** * The Session holds the managers of the session. */ static class Session extends SessionSingleton { protected ConcurrentMap<String, LayerFeatureBufferManager> managers = new ConcurrentHashMap(); public static Session instance() { return instance(Session.class); } } /** * Gets the buffer manager for the given layer of the current session. If no * manager exists yet a new one is created with default buffer type/impl and * settings if <code>create</code> is true, otherwise null might be returned. * * @param layer * @param create True specifies that a new buffer manager is created if * necessary. * @return The buffer manager for the given layer. */ public static LayerFeatureBufferManager forLayer(ILayer layer, boolean create) { assert layer != null; ConcurrentMap<String, LayerFeatureBufferManager> managers = Session.instance().managers; LayerFeatureBufferManager result = managers.get(layer.id()); if (result == null && create) { result = new LayerFeatureBufferManager(layer); LayerFeatureBufferManager prev = managers.putIfAbsent(layer.id(), result); result = prev != null ? prev : result; assert result.getLayer() == layer; } return result; } // instance ******************************************* private ILayer layer; private IFeatureBuffer buffer; private FeatureBufferProcessor processor; private Transaction tx; private long layerTimestamp; /** The feature event assembled during {@link #prepareSave(OperationSupport, IProgressMonitor)}. */ private Updater updater; protected LayerFeatureBufferManager(ILayer layer) { this.layer = layer; this.layerTimestamp = System.currentTimeMillis(); buffer = new MemoryFeatureBuffer(); buffer.init(new IFeatureBufferSite() { @Override public void fireFeatureChangeEvent(Type type, Collection<Feature> features) { LayerFeatureBufferManager.this.fireFeatureChangeEvent(type, features); } @Override public void revert(Filter filter, IProgressMonitor monitor) { LayerFeatureBufferManager.this.revert(filter, monitor); } }); processor = new FeatureBufferProcessor(this, buffer); if (Polymap.getSessionDisplay() != null) { OperationSupport.instance().addOperationSaveListener(this); } } protected void fireFeatureChangeEvent(FeatureChangeEvent.Type type, Collection<Feature> features) { FidSet fids = new FidSet(features.size() * 2); for (Feature feature : features) { fids.add(feature.getIdentifier()); } FeatureChangeEvent ev = new FeatureChangeEvent(layer, type, fids); EventManager.instance().publish(ev); } public ILayer getLayer() { return layer; } public IFeatureBuffer getBuffer() { return buffer; } public FeatureBufferProcessor getProcessor() { return processor; } public long getLayerTimestamp() { return layerTimestamp; } public void save(OperationSupport os, IProgressMonitor monitor) { if (tx == null) { return; } try { monitor.beginTask(layer.getLabel(), buffer.size()); try { tx.commit(); buffer.clear(); updater.apply(layer); layerTimestamp = updater.getStartTime(); } finally { tx.close(); tx = null; updater.done(); updater = null; } monitor.done(); } catch (Exception e) { PolymapWorkbench.handleError(DataPlugin.PLUGIN_ID, this, "Unable to commit the transaction. The data has inconsistent state! Please contact the administrator.", e); } } public void rollback(OperationSupport os, IProgressMonitor monitor) { if (tx == null) { return; } try { monitor.beginTask(layer.getLabel(), buffer.size()); try { tx.rollback(); } finally { tx.close(); tx = null; updater.done(); updater = null; } monitor.done(); } catch (Exception e) { PolymapWorkbench.handleError(DataPlugin.PLUGIN_ID, this, "Unable to rollback transaction. The data has inconsistent state! Please contact the administrator.", e); } } public void prepareSave(OperationSupport os, IProgressMonitor monitor) throws Exception { if (tx != null) { throw new IllegalStateException("Pending transaction found."); } if (buffer.isEmpty()) { return; } try { monitor.beginTask(layer.getLabel(), buffer.size()); } catch (NoSuchEntityException e) { // the layer was deleted meanwhile return; } tx = new DefaultTransaction("Submit buffer: layer-" + layer.id() + "-" + System.currentTimeMillis()); // store directly to the underlying store; so the buffer processor MUST be // the first processor after the DataStoreProcessor IGeoResource geores = layer.getGeoResource(); FeatureStore fs = geores.resolve(FeatureStore.class, null); if (fs == null) { throw new IOException("Unable to write to the data source of layer: " + layer.getLabel()); } fs.setTransaction(tx); updater = EntityStateTracker.instance().newUpdater(); FeatureCollection added = FeatureCollections.newCollection(); Set<Identifier> removed = new HashSet(); int count = 0; for (FeatureBufferState buffered : buffer.content()) { if (monitor.isCanceled()) { return; } if ((++count % 100) == 0) { monitor.subTask("(" + count + ")"); } // update feature timestamp (check concurrent modifications within this JVM) updater.checkSet(buffered.handle(), buffered.timestamp(), null); if (buffered.isAdded()) { // no check if fid was created already since it is propably the 'primary key' added.add(buffered.feature()); } else if (buffered.isModified()) { checkSubmitModified(fs, buffered); } else if (buffered.isRemoved()) { removed.add(buffered.feature().getIdentifier()); } else { log.warn("Buffered feature is not added/removed/modified!"); } monitor.worked(1); } if (!added.isEmpty()) { fs.addFeatures(added); } if (!removed.isEmpty()) { fs.removeFeatures(ff.id(removed)); } // none of the features had concurrent modifications, so just upgrade // timestamp for the layer (no checking is needed and done) if (count > 0) { EntityHandle layerHandle = FeatureStateTracker.layerHandle(layer); updater.checkSet(layerHandle, updater.getStartTime(), null); } monitor.done(); } @Override public void revert(OperationSupport os, IProgressMonitor monitor) { revert(Filter.INCLUDE, monitor); } /** * * @param filter Specifies what features to revert. null for all features. * @param monitor */ public void revert(Filter filter, IProgressMonitor monitor) { assert filter != null; try { monitor.beginTask(layer.getLabel(), buffer.size()); List<Feature> reverted = new ArrayList(buffer.size()); for (FeatureBufferState buffered : buffer.content()) { if (filter.evaluate(buffered.original())) { buffer.unregisterFeatures(Collections.singletonList(buffered.feature())); reverted.add(buffered.feature()); } } fireFeatureChangeEvent(FeatureChangeEvent.Type.FLUSHED, reverted); monitor.done(); } catch (Exception e) { PolymapWorkbench.handleError(DataPlugin.PLUGIN_ID, this, "Unable to revert changes.", e); } } protected void checkSubmitModified(FeatureStore fs, FeatureBufferState buffered) throws Exception { // check concurrent modifications with the store FeatureId fid = buffered.feature().getIdentifier(); Id fidFilter = ff.id(Collections.singleton(fid)); List<Feature> stored = loadFeatures(fs, fidFilter); if (stored.size() == 0) { throw new ConcurrentModificationException("Feature has been removed concurrently: " + fid); } else if (stored.size() > 1) { throw new IllegalStateException("More than one feature for id: " + fid + "!?"); } if (isFeatureModified(stored.get(0), buffered.original())) { throw new ConcurrentModificationException( "Objekt wurde von einem anderen Nutzer gleichzeitig gendert: " + fid); } // write down AttributeDescriptor[] type = {}; Object[] value = {}; for (Property origProp : buffered.original().getProperties()) { if (origProp.getDescriptor() instanceof AttributeDescriptor) { Property newProp = buffered.feature().getProperty(origProp.getName()); if (isPropertyModified(origProp.getValue(), newProp.getValue())) { type = (AttributeDescriptor[]) ArrayUtils.add(type, origProp.getDescriptor()); value = ArrayUtils.add(value, newProp.getValue()); log.info("Attribute modified: " + origProp.getDescriptor().getName() + " = " + newProp.getValue() + " (" + fid.getID() + ")"); } } } fs.modifyFeatures(type, value, fidFilter); } private List<Feature> loadFeatures(FeatureStore fs, Filter filter) throws IOException { List<Feature> result = new ArrayList(); FeatureCollection features = fs.getFeatures(filter); Iterator it = null; try { for (it = features.iterator(); it.hasNext();) { result.add((Feature) it.next()); } } finally { features.close(it); } return result; } private boolean isFeatureModified(Feature feature, Feature original) throws IOException { // XXX complex features SimpleFeatureType schema = ((SimpleFeature) original).getType(); for (AttributeDescriptor attribute : schema.getAttributeDescriptors()) { Object value1 = ((SimpleFeature) feature).getAttribute(attribute.getName()); Object value2 = ((SimpleFeature) original).getAttribute(attribute.getName()); if (isPropertyModified(value1, value2)) { return true; } } return false; } private boolean isPropertyModified(Object value1, Object value2) { if (value1 instanceof Geometry) { if (!((Geometry) value1).equalsExact((Geometry) value2)) { return true; } } else if ((value1 != null && !value1.equals(value2)) || value1 == null && value2 != null) { return true; } return false; } }