/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.gwc; import static com.google.common.base.Preconditions.checkNotNull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.namespace.QName; import net.opengis.wfs.DeleteElementType; import net.opengis.wfs.InsertElementType; import net.opengis.wfs.TransactionResponseType; import net.opengis.wfs.TransactionType; import net.opengis.wfs.UpdateElementType; import org.eclipse.emf.ecore.EObject; import org.geoserver.wfs.TransactionEvent; import org.geoserver.wfs.TransactionEventType; import org.geoserver.wfs.TransactionPlugin; import org.geoserver.wfs.WFSException; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.geometry.jts.ReferencedEnvelope3D; import org.geotools.referencing.CRS; import org.geotools.util.logging.Logging; import org.geowebcache.GeoWebCacheException; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; /** * Listens to transactions (so far only issued by WFS) and truncates the cache for the affected area * of the layers involved in the transaction. * <p> * A Spring bean singleton of this class needs to be declared in order for GeoServer transactions to * pick it up automatically and forward transaction events to it. * </p> * <p> * TODO: upon deletion, only truncate if feature count > 0 * </p> * * @author Arne Kepp * @author Gabriel Roldan * @version $Id$ * */ public class GWCTransactionListener implements TransactionPlugin { private static Logger log = Logging.getLogger(GWCTransactionListener.class); final private GWC gwc; static final String GWC_TRANSACTION_INFO_PLACEHOLDER = "GWC_TRANSACTION_INFO_PLACEHOLDER"; /** * @param gwc */ public GWCTransactionListener(final GWC gwc) { this.gwc = gwc; } /** * Not used, we're interested in the {@link #dataStoreChange} and {@link #afterTransaction} * hooks * * @see org.geoserver.wfs.TransactionPlugin#beforeTransaction(net.opengis.wfs.TransactionType) */ public TransactionType beforeTransaction(TransactionType request) throws WFSException { // nothing to do return request; } /** * Not used, we're interested in the {@link #dataStoreChange} and {@link #afterTransaction} * hooks * * @see org.geoserver.wfs.TransactionPlugin#beforeCommit(net.opengis.wfs.TransactionType) */ public void beforeCommit(TransactionType request) throws WFSException { // nothing to do } /** * If transaction's succeeded then truncate the affected layers at the transaction affected * bounds * * @see org.geoserver.wfs.TransactionPlugin#afterTransaction */ public void afterTransaction(final TransactionType request, TransactionResponseType result, boolean committed) { if (!committed) { return; } try { afterTransactionInternal(request, committed); } catch (RuntimeException e) { // Do never make the transaction fail due to a GWC error. Yell on the logs though log.log(Level.WARNING, "Error trying to truncate the transaction affected area", e); } } private void afterTransactionInternal(final TransactionType transaction, boolean committed) { final Map<String, List<ReferencedEnvelope>> byLayerDirtyRegions = getByLayerDirtyRegions(transaction); if (byLayerDirtyRegions.isEmpty()) { return; } for (String tileLayerName : byLayerDirtyRegions.keySet()) { List<ReferencedEnvelope> dirtyList = byLayerDirtyRegions.get(tileLayerName); ReferencedEnvelope dirtyRegion; try { dirtyRegion = merge(tileLayerName, dirtyList); } catch (Exception e) { log.log(Level.WARNING, e.getMessage(), e); continue; } if (dirtyRegion == null) { continue; } try { gwc.truncate(tileLayerName, dirtyRegion); } catch (GeoWebCacheException e) { log.warning("Error truncating tile layer " + tileLayerName + " for transaction affected bounds " + dirtyRegion); } } } private ReferencedEnvelope merge(final String tileLayerName, final List<ReferencedEnvelope> dirtyList) throws TransformException, FactoryException { if (dirtyList.size() == 0) { return null; } final CoordinateReferenceSystem declaredCrs = CRS.getHorizontalCRS(gwc.getDeclaredCrs(tileLayerName)); ReferencedEnvelope merged = new ReferencedEnvelope(declaredCrs); for (ReferencedEnvelope env : dirtyList) { if(env instanceof ReferencedEnvelope3D) { env = new ReferencedEnvelope(env, CRS.getHorizontalCRS(env.getCoordinateReferenceSystem())); } ReferencedEnvelope transformedDirtyRegion = env.transform(declaredCrs, true, 1000); merged.expandToInclude(transformedDirtyRegion); } return merged; } /** * @return {@code 0}, we don't need any special treatment * @see org.geoserver.wfs.TransactionPlugin#getPriority() */ public int getPriority() { return 0; } /** * Collects the per TileLayer affected bounds * * @see org.geoserver.wfs.TransactionListener#dataStoreChange(org.geoserver.wfs.TransactionEvent) */ public void dataStoreChange(final TransactionEvent event) throws WFSException { log.info("DataStoreChange: " + event.getLayerName() + " " + event.getType()); try { dataStoreChangeInternal(event); } catch (RuntimeException e) { // Do never make the transaction fail due to a GWC error. Yell on the logs though log.log(Level.WARNING, "Error pre computing the transaction's affected area", e); } } private void dataStoreChangeInternal(final TransactionEvent event) { final Object source = event.getSource(); if (!(source instanceof InsertElementType || source instanceof UpdateElementType || source instanceof DeleteElementType)) { return; } final EObject originatingTransactionRequest = (EObject) source; checkNotNull(originatingTransactionRequest, "No original transaction request exists"); final TransactionEventType type = event.getType(); if (TransactionEventType.POST_INSERT.equals(type)) { // no need to compute the bounds, they're the same than for PRE_INSERT return; } final QName featureTypeName = event.getLayerName(); final Set<String> affectedTileLayers = gwc.getTileLayersByFeatureType( featureTypeName.getNamespaceURI(), featureTypeName.getLocalPart()); if (affectedTileLayers.isEmpty()) { // event didn't touch a cached layer return; } final SimpleFeatureCollection affectedFeatures = event.getAffectedFeatures(); final ReferencedEnvelope affectedBounds = affectedFeatures.getBounds(); final TransactionType transaction = event.getRequest(); for (String tileLayerName : affectedTileLayers) { addLayerDirtyRegion(transaction, tileLayerName, affectedBounds); } } @SuppressWarnings("unchecked") private Map<String, List<ReferencedEnvelope>> getByLayerDirtyRegions( final TransactionType transaction) { final Map<Object, Object> extendedProperties = transaction.getExtendedProperties(); Map<String, List<ReferencedEnvelope>> byLayerDirtyRegions; byLayerDirtyRegions = (Map<String, List<ReferencedEnvelope>>) extendedProperties .get(GWC_TRANSACTION_INFO_PLACEHOLDER); if (byLayerDirtyRegions == null) { byLayerDirtyRegions = new HashMap<String, List<ReferencedEnvelope>>(); extendedProperties.put(GWC_TRANSACTION_INFO_PLACEHOLDER, byLayerDirtyRegions); } return byLayerDirtyRegions; } private void addLayerDirtyRegion(final TransactionType transaction, final String tileLayerName, final ReferencedEnvelope affectedBounds) { Map<String, List<ReferencedEnvelope>> byLayerDirtyRegions = getByLayerDirtyRegions(transaction); List<ReferencedEnvelope> layerDirtyRegion = byLayerDirtyRegions.get(tileLayerName); if (layerDirtyRegion == null) { layerDirtyRegion = new ArrayList<ReferencedEnvelope>(2); byLayerDirtyRegions.put(tileLayerName, layerDirtyRegion); } layerDirtyRegion.add(affectedBounds); } }