org.geoserver.wfs.notification.WFSNotify.java Source code

Java tutorial

Introduction

Here is the source code for org.geoserver.wfs.notification.WFSNotify.java

Source

/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved.
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */

package org.geoserver.wfs.notification;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.WeakHashMap;

import javax.xml.namespace.QName;

import net.opengis.wfs.TransactionResponseType;
import net.opengis.wfs.TransactionType;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.wfs.TransactionEvent;
import org.geoserver.wfs.TransactionEventType;
import org.geoserver.wfs.TransactionListener;
import org.geoserver.wfs.TransactionPlugin;
import org.geoserver.wfs.WFSException;
import org.geoserver.wfs.notification.TriggerManager.TriggerCallback;
import org.geotools.data.FeatureSource;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.identity.Identifier;
import org.springframework.beans.factory.DisposableBean;

/**
 * <p>
 * A {@link TransactionPlugin} and {@link TransactionListener} that receives
 * updates from an ongoing transaction and performs some post-processing on
 * those updates to send out related notifications. This class binds a
 * {@link TransactionStatus} to the the transaction that contains some
 * bookkeeping information. This is only possible due to some extensions to
 * GeoServer.... (TODO: contribute this back in a form acceptable to OpenGeo.)
 * <p>
 * This happens in two phases: pre-delete and post-insert. Pre-delete, related
 * notifications need to be found right then (related feature types queried)
 * because related information might be stored in views. Post-delete, that
 * information will already be gone from the database view.
 * <p>
 * This situation could also potentially happen with inserts, but only the
 * post-insert case is handled right now. TODO ...
 * <p>
 * "Related notifications" to be sent over WSN are accumulated in
 * {@link TransactionStatus} during pre-delete events. Features that need to be
 * processed are also accumulated there during post-insert events. Both types
 * are finally handled during {@link #beforeCommit}.
 * <p>
 * This class is meant to be used in a managed-object container, like Spring.
 * {@link #setTriggerManager(TriggerManager)} must be called after construction with a non-null
 * {@link TriggerManager}, followed by a call to {@link #init()}, at the very least,
 * for objects of this class to minimally functional.
 * <p>
 * To-do items:
 * <ul>
 * <li>TODO: GeoServer doesn't send post-delete notifications, which could be
 * useful.</li>
 * <li>TODO: Handle pre-insert, post-delete cases here.</li>
 * <li>TODO: Contribute extended {@link TransactionPlugin},
 * {@link TransactionListener} classes back to OpenGeo.</li>
 * </ul>
 *
 * @author Dustin Parker &lt;dparker@forwardslope.com&gt;
 */
public class WFSNotify implements TransactionPlugin, TransactionListener, DisposableBean {
    private static final Log LOG = LogFactory.getLog(WFSNotify.class);
    private static final FilterFactory FF = CommonFactoryFinder.getFilterFactory(null);

    /**
     * Class that lets code 'spy' on features as they're removed from a {@link FeatureIterator}.
     * 
     * @author dparker
     */
    private static class FeatureIteratorWrapper<F extends Feature> implements FeatureIterator<F> {
        protected final FeatureIterator<F> fi;

        public FeatureIteratorWrapper(FeatureIterator<F> fi) {
            this.fi = fi;
        }

        @Override
        public boolean hasNext() {
            return fi.hasNext();
        }

        @Override
        public F next() throws NoSuchElementException {
            return fi.next();
        }

        @Override
        public void close() {
            fi.close();
        }
    }

    private static class CollectionsFeatureIterator<F extends Feature> implements FeatureIterator<F> {
        protected final Iterator<F> fi;

        public CollectionsFeatureIterator(Iterator<F> fi) {
            this.fi = fi;
        }

        @Override
        public boolean hasNext() {
            return fi.hasNext();
        }

        @Override
        public F next() {
            return fi.next();
        }

        @Override
        public void close() {
        }
    }

    protected Catalog catalog;
    protected TriggerManager tm;
    protected GMLNotificationSerializer cb;
    protected NotificationSerializer serializer;
    protected Collection<NotificationPublisher> publishers;
    protected final boolean publishDebug = Boolean
            .parseBoolean(System.getProperty("com.fsi.c2rpc.geoserver.wsn.PublishCallback.debug"));

    private final Map<Object, TransactionStatus> statuses = Collections.synchronizedMap(new WeakHashMap());

    public void setCatalog(Catalog catalog) {
        this.catalog = catalog;
    }

    public void setTriggerManager(TriggerManager tm) {
        this.tm = tm;
    }

    public void setCallback(GMLNotificationSerializer cb) {
        this.cb = cb;
    }

    public void init() {
        if (tm == null) {
            throw new IllegalStateException("TriggerManager was not set, which is required.");
        }
        List<NotificationSerializer> serializers = GeoServerExtensions.extensions(NotificationSerializer.class);
        if (serializers.isEmpty()) {
            LOG.warn("No WFS notification serializers found.");
        } else {
            if (serializers.size() > 1) {
                StringBuilder msg = new StringBuilder("Multiple WFS notification serializers found:\n");
                for (NotificationSerializer serializer : serializers) {
                    msg.append('\t').append(serializer.getClass().getName()).append('\n');
                }
                msg.append("Using first.");
                LOG.warn(msg);
            }
            serializer = serializers.get(0);
        }

        publishers = GeoServerExtensions.extensions(NotificationPublisher.class);

        if (publishers.isEmpty()) {
            LOG.info("No notification publishers found.");
        } else {
            StringBuilder msg = new StringBuilder("Using the following WFS notification publisher(s):\n");
            for (NotificationPublisher publisher : publishers) {
                msg.append('\t').append(publisher.getClass().getName()).append('\n');
            }
            LOG.info(msg);
        }
    }

    public void dataStoreChange(TransactionEvent event) throws WFSException {
        try {
            // don't notify if we don't have to.
            boolean ready = false;
            for (NotificationPublisher publisher : publishers) {
                ready |= publisher.isReady();
            }
            if (!(ready || publishDebug)) {
                LOG.debug("No publishers ready, skipping notification.");
                return;
            }

            if (serializer == null) {
                return;
            }

            final TransactionStatus ts = statuses.get(event.getRequest());

            if (ts == null)
                return;

            ts.setTransaction(event.getTransaction());

            if (event.getType() == TransactionEventType.PRE_INSERT) {
                preInsert(event, ts);
            } else if (event.getType() == TransactionEventType.PRE_DELETE) {
                preDelete(event, ts);
            } else if (event.getType() == TransactionEventType.POST_INSERT) {
                postInsert(event, ts);
            } else if (event.getType() == TransactionEventType.PRE_UPDATE) {
                preUpdate(event, ts);
            }
        } catch (Throwable t) {
            handleException(t);
        }
    }

    private void handleException(Throwable t) {
        final String message = "Caught throwable during notification attempt, won't rethrow.";
        if (LOG.isDebugEnabled())
            LOG.debug(message, t);
        else
            LOG.warn(message);
    }

    protected void preUpdate(TransactionEvent event, TransactionStatus ts) throws Exception {
    }

    protected void preInsert(TransactionEvent event, final TransactionStatus ts) throws Exception {
        FeatureIterator<SimpleFeature> affected = getAffectedFeatures(event);
        try {
            affected = new FeatureIteratorWrapper<SimpleFeature>(affected) {
                @Override
                public SimpleFeature next() throws NoSuchElementException {
                    SimpleFeature next = super.next();
                    ts.affected(next);
                    return next;
                }
            };

            tm.triggerEvent(affected, getLayerName(event), new TriggerCallback() {

                @Override
                public void triggerEvent(Feature f) {
                    ts.modified(f);
                }
            }, ts.getTransaction());

        } finally {
            affected.close();
        }
    }

    protected void postInsert(TransactionEvent event, TransactionStatus ts) throws Exception {
    }

    protected void preDelete(final TransactionEvent event, final TransactionStatus ts) throws Exception {
        FeatureIterator<SimpleFeature> affected = getAffectedFeatures(event);
        try {
            affected = new FeatureIteratorWrapper<SimpleFeature>(affected) {
                @Override
                public SimpleFeature next() throws NoSuchElementException {
                    SimpleFeature next = super.next();
                    ts.affected(next);
                    return next;
                }
            };

            tm.triggerEvent(affected, getLayerName(event), new TriggerCallback() {
                @Override
                public void triggerEvent(Feature f) {
                    ts.modified(f);
                }
            }, ts.getTransaction());
        } finally {
            affected.close();
        }
    }

    /**
     * Find all features that are about to be inserted, deleted, or updated by this event.
     */
    protected static FeatureIterator<SimpleFeature> getAffectedFeatures(TransactionEvent event) {
        FeatureCollection<SimpleFeatureType, SimpleFeature> collection = event.getAffectedFeatures();
        return collection.features();
    }

    protected final QName getLayerName(TransactionEvent event) {
        final QName layerName;
        QName name = event.getLayerName();
        // Normalize the QName if needed
        if (name.getPrefix() == null || name.getPrefix().length() == 0) {
            NamespaceInfo info = catalog.getNamespaceByURI(name.getNamespaceURI());
            if (info == null) {
                throw new NoSuchElementException("No such namespace in our catalog: " + name.getNamespaceURI());
            }
            name = new QName(name.getNamespaceURI(), name.getLocalPart(), info.getPrefix());
        }
        layerName = name;
        return layerName;
    }

    public void destroy() throws Exception {
        LOG.info("GeoServer WSN producer is being destroyed.");
    }

    @Override
    public TransactionType beforeTransaction(TransactionType request) throws WFSException {
        try {
            statuses.put(request, (TransactionStatus) new TransactionStatus());
        } catch (Throwable t) {
            handleException(t);
        }
        return request;
    }

    /*
    public void beginTransaction(TransactionType request, Object userData, Transaction transaction) {
    try {
        TransactionStatus ts = (TransactionStatus) userData;
        ts.transaction = transaction;
    } catch(Throwable t) {
        handleException(t);
    }
    }
    */

    @Override
    public void afterTransaction(TransactionType request, TransactionResponseType result, boolean committed) {
        try {
            TransactionStatus ts = statuses.remove(request);
            if (ts != null) {
                ts.destroy();
            }
        } catch (Throwable t) {
            handleException(t);
        }
    }

    @Override
    public void beforeCommit(TransactionType request) throws WFSException {
        try {
            TransactionStatus ts = statuses.get(request);
            if (ts != null) {
                tryBeforeCommit(request, ts);
            }
        } catch (Throwable t) {
            handleException(t);
        }
    }

    public void tryBeforeCommit(TransactionType request, final TransactionStatus ts) throws WFSException {

        // Rerun each query to be checked
        for (Entry<Name, Set<Identifier>> ent : ts.getAffected().entrySet()) {
            Collection<Feature> features = new ArrayList(ent.getValue().size());
            for (Identifier id : ent.getValue()) {
                features.add(ts.getFidMap().get(id));
            }
            try {
                tm.triggerEvent(new CollectionsFeatureIterator<Feature>(features.iterator()),
                        new QName(ent.getKey().getNamespaceURI(), ent.getKey().getLocalPart()),
                        new TriggerCallback() {
                            @Override
                            public void triggerEvent(Feature f) {
                                if (ts.checkFeature(f)) {
                                    WFSNotify.this.triggerEvent(f);
                                }
                            }
                        }, ts.getTransaction());
            } catch (IOException e) {
                LOG.debug("Error checking modified features, notifications will be inaccurate:", e);
            }
        }

        // Check the remaining features in the ts map:
        for (Entry<Name, Set<Identifier>> ent : ts.getPotentiallyModified().entrySet()) {
            try {
                if (ent.getValue().isEmpty()) {
                    continue; // Nothing to do...
                }
                FeatureTypeInfo info = catalog.getFeatureTypeByName(ent.getKey());
                if (info == null) {
                    continue;
                }

                // Create a FID query for each type that's not empty, do the same as the above
                Filter filter = FF.id(ent.getValue());

                FeatureSource<? extends FeatureType, ? extends Feature> source = info.getFeatureSource(null, null);
                FeatureCollection<? extends FeatureType, ? extends Feature> coll = source.getFeatures(filter);
                FeatureIterator<? extends Feature> i = coll.features();

                try {
                    while (i.hasNext()) {
                        Feature f = i.next();
                        if (ts.checkFeature(f)) {
                            triggerEvent(f);
                        }
                    }
                } finally {
                    i.close();
                }

            } catch (IOException e) {
            }
        }

        // Delete anything left in the ts map
        for (Entry<Name, Set<Identifier>> ent : ts.getPotentiallyModified().entrySet()) {
            Name typeName = ent.getKey();
            for (Identifier id : ent.getValue()) {
                triggerDeleteEvent(typeName, id);
            }
        }

    }

    private void triggerEvent(Feature f) {
        if (serializer != null) {
            publishEvent(serializer.serializeInsertOrUpdate(f));
        }
    }

    private void triggerDeleteEvent(Name typeName, Identifier id) {
        if (serializer != null) {
            publishEvent(serializer.serializeDelete(typeName, id));
        }
    }

    private void publishEvent(String msg) {
        for (NotificationPublisher publisher : publishers) {
            if (publisher.isReady()) {
                publisher.publish(msg);
            }
        }
    }

    public int getPriority() {
        return 0;
    }

    public static Name getTypeName(Feature f) {
        if (f.getDescriptor() != null)
            return f.getDescriptor().getName();
        if (f.getType() != null)
            return f.getType().getName();
        return null;
    }
}