package org.geoserver.geosync; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Iterator; import java.util.ArrayList; import java.util.List; import java.util.TreeSet; import java.util.Set; import java.util.Map; import java.util.HashMap; import java.util.Date; import java.io.File; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.io.FileWriter; import java.io.StringReader; import java.awt.geom.Rectangle2D; import javax.servlet.http.HttpServletRequest; import org.geoserver.wfs.TransactionEventType; import org.geoserver.wfs.TransactionListener; import org.geoserver.wfs.WFSException; import org.geoserver.wfs.TransactionEvent; import org.geoserver.wfs.xml.v1_1_0.WFS; import org.geoserver.wfs.xml.v1_1_0.WFSConfiguration; import org.vfny.geoserver.global.GeoserverDataDirectory; import org.vfny.geoserver.global.ConfigurationException; import org.geotools.xml.Encoder; import org.geotools.xml.Parser; import org.geotools.xs.bindings.XSDateTimeBinding; import org.geotools.feature.FeatureCollection; import org.geotools.geometry.jts.ReferencedEnvelope; import org.apache.xml.serialize.OutputFormat; import net.opengis.wfs.WfsFactory; import net.opengis.wfs.TransactionType; import net.opengis.wfs.InsertElementType; import net.opengis.wfs.DeleteElementType; import net.opengis.wfs.UpdateElementType; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import com.sun.syndication.feed.synd.SyndFeed; import com.sun.syndication.feed.synd.SyndFeedImpl; import com.sun.syndication.feed.synd.SyndEntry; import com.sun.syndication.feed.synd.SyndEntryImpl; import com.sun.syndication.feed.synd.SyndContent; import com.sun.syndication.feed.synd.SyndContentImpl; import com.sun.syndication.feed.synd.SyndLink; import com.sun.syndication.feed.synd.SyndLinkImpl; import com.sun.syndication.io.SyndFeedInput; import com.sun.syndication.io.XmlReader; import com.sun.syndication.io.SyndFeedOutput; import com.sun.syndication.propono.atom.common.AtomService; import com.sun.syndication.propono.atom.common.Workspace; import com.sun.syndication.propono.atom.common.Collection; import com.sun.syndication.feed.module.georss.GMLModuleImpl; import com.sun.syndication.feed.module.georss.GeoRSSModule; import com.sun.syndication.feed.module.georss.GeoRSSUtils; import com.sun.syndication.feed.module.georss.SimpleModuleImpl; import com.sun.syndication.feed.module.georss.geometries.Position; import com.sun.syndication.feed.module.georss.geometries.Envelope; public class RecordingTransactionListener implements TransactionListener{ private static Set recordWorthyEvents; private List myHistory; private WFSConfiguration xmlConfiguration; static { recordWorthyEvents = new TreeSet(); recordWorthyEvents.add(TransactionEventType.PRE_INSERT); recordWorthyEvents.add(TransactionEventType.PRE_UPDATE); recordWorthyEvents.add(TransactionEventType.PRE_DELETE); } public RecordingTransactionListener(){ myHistory = new ArrayList(); } public void dataStoreChange(TransactionEvent event) throws WFSException{ if ( recordWorthyEvents.contains( event.getType() ) && !event.getAffectedFeatures().isEmpty() ) { try { String layer = event.getLayerName().getLocalPart(); SyndFeed feed = readFeed(layer); List l = feed.getEntries(); SyndEntry entry = eventToEntry(event); l.add(entry); feed.setEntries(l); saveFeed(layer,feed); } catch (Exception e){ e.printStackTrace(); // LOG ERROR HERE!! } } } public WFSConfiguration getWFSConfig(){ return xmlConfiguration; } public void setWFSConfig(WFSConfiguration wfsc){ xmlConfiguration = wfsc; } public File getFile( String layer ) throws ConfigurationException{ File f = GeoserverDataDirectory.findCreateConfigDir("geosync"); return new File(f, layer + "-history.xml"); } public SyndFeed readFeed(String layer) throws Exception{ //create an empty feed SyndFeed feed; File file = getFile(layer); if ( file.exists() ) { //read the feed from disk filtering as need be synchronized ( this ) { SyndFeedInput input = new SyndFeedInput(); feed = input.build(new XmlReader(getFile(layer))); } } else { //create a new empty feed feed = new SyndFeedImpl(); feed.setFeedType("atom_1.0"); feed.setTitle(layer + " changes"); } return feed; } public SyndFeed filterFeed(String layer, Map params, String baseUrl,HttpServletRequest req) throws Exception{ SyndFeed feed = readFeed(layer); HistoryFilter filter = getFilter(params); List entries = feed.getEntries(); for (int i = 0; i < entries.size(); i++){ SyndEntry entry = (SyndEntry)entries.get(i); if (!filter.pass(entry)){ entries.remove(i); i--; } } //set the link element SyndLink link = new SyndLinkImpl(); link.setHref(req.getRequestURL().toString()); feed.setLink(link); return feed; } public void saveFeed(String layer, SyndFeed feed) throws Exception{ File f = getFile(layer); synchronized ( this ) { PrintWriter writer = new PrintWriter(new FileWriter(f)); SyndFeedOutput out = new SyndFeedOutput(); out.output(feed, writer); } } public SyndEntry eventToEntry(TransactionEvent evt) throws Exception{ SyndEntry entry = new SyndEntryImpl(); entry.setTitle("Changes to " + evt.getLayerName().getLocalPart()); // entry.setLink("http://geoserver.org/a"); entry.setPublishedDate(new Date()); //encode the content as the wfs transcation SyndContent description = new SyndContentImpl(); description.setType("text/xml"); description.setValue(encodeTransaction( evt )); // attach the content to the entry List contents = new ArrayList(); contents.add(description); entry.setContents(contents); // Add the georss info ReferencedEnvelope refenv = evt.getAffectedFeatures().getBounds(); GeoRSSModule geoInfo = new GMLModuleImpl(); double minLat = refenv.getMinimum(0), minLong = refenv.getMinimum(1), maxLat = refenv.getMaximum(0), maxLong = refenv.getMaximum(1); Envelope bounds = new Envelope(minLat, minLong, maxLat, maxLong); geoInfo.setGeometry(bounds); List modules = entry.getModules(); modules.add(geoInfo); entry.setModules(modules); return entry; } String encodeTransaction( TransactionEvent e ) throws IOException { TransactionType tx = WfsFactory.eINSTANCE.createTransactionType(); Object source = e.getSource(); if ( source instanceof InsertElementType ) { tx.getInsert().add( source ); } else if ( source instanceof UpdateElementType ) { tx.getUpdate().add( source ); } else if ( source instanceof DeleteElementType ) { tx.getDelete().add( source ); } Encoder encoder = new Encoder( xmlConfiguration ); OutputFormat of = new OutputFormat(); of.setOmitXMLDeclaration( true ); encoder.setOutputFormat( of ); ByteArrayOutputStream out = new ByteArrayOutputStream(); encoder.encode( tx, WFS.TRANSACTION, out ); return new String( out.toByteArray() ); } public List getHistoryList(String layername){ List matches = new ArrayList(); Iterator it = myHistory.iterator(); while (it.hasNext()){ TransactionEvent e = (TransactionEvent) it.next(); if ( e.getLayerName().equals( layername ) ) { matches.add( e ); } } return matches; } public List getFullHistoryList(){ return myHistory; } private HistoryFilter getFilter(Map filterParams){ Iterator it = filterParams.entrySet().iterator(); List filters = new ArrayList(); Date start, end = null; while (it.hasNext()){ Map.Entry entry = (Map.Entry)it.next(); String key = (String) entry.getKey(); String value = (String) entry.getValue(); if ( "bbox".equalsIgnoreCase( key ) ) { BBoxFilter f = new BBoxFilter(); f.initialize( value ); filters.add( f ); } else if ( "startIndex".equalsIgnoreCase( key ) ) { int index = Integer.parseInt( (String) entry.getValue() ); filters.add( new IndexFilter( index ) ); } else if ( "dtstart".equalsIgnoreCase( key ) ) { DateFilter filter = new DateFilter( DateFilter.BEFORE ); filter.initialize( value ); filters.add( filter ); } else if ( "dtend".equalsIgnoreCase( key) ) { DateFilter filter = new DateFilter( DateFilter.AFTER ); filter.initialize( value ); filters.add( filter ); } } return new CompositeFilter(filters); } public interface HistoryFilter { public void initialize(String param); public boolean pass(SyndEntry entry); } public class CompositeFilter implements HistoryFilter { private List myFilters; public CompositeFilter(List filters){ myFilters = filters; } public void initialize(String param){ // no need } public boolean pass(SyndEntry entry){ Iterator it = myFilters.iterator(); while (it.hasNext()){ HistoryFilter f = (HistoryFilter)it.next(); if (f != null && !f.pass(entry)){ return false; } } return true; } } public class IndexFilter implements HistoryFilter { int index; int counter = 0; public IndexFilter( int index ) { this.index = index; } public void initialize(String param) { } public boolean pass(SyndEntry entry) { return ++counter >= index; } } public class DateFilter implements HistoryFilter{ public static final int BEFORE = 0; public static final int AFTER = 1; Date myDate; int rel; public DateFilter( int rel ) { this.rel = rel; } public void initialize(String param){ try { myDate = ((Calendar) new XSDateTimeBinding().parse(null, param )).getTime(); } catch (Exception e) { throw new RuntimeException( e ); } } public boolean pass(SyndEntry entry){ switch ( rel ) { case BEFORE: return myDate.before( entry.getUpdatedDate() ); case AFTER: return myDate.after( entry.getUpdatedDate() ); } throw new RuntimeException(); } } public class BBoxFilter implements HistoryFilter{ //Rectangle2D myBBox; com.vividsolutions.jts.geom.Envelope myBBox; public void initialize(String param){ String[] parts = param.split(","); double minLat = Double.valueOf(parts[0]); double minLon = Double.valueOf(parts[1]); double maxLat = Double.valueOf(parts[2]); double maxLon = Double.valueOf(parts[3]); myBBox = new com.vividsolutions.jts.geom.Envelope(minLon, maxLon, minLat,maxLat); } public boolean pass(SyndEntry entry){ GeoRSSModule geo = GeoRSSUtils.getGeoRSS(entry); if (geo != null && myBBox != null && geo.getGeometry() instanceof Envelope){ Envelope env = (Envelope) geo.getGeometry(); com.vividsolutions.jts.geom.Envelope box = new com.vividsolutions.jts.geom.Envelope( env.getMinLongitude(), env.getMaxLongitude(), env.getMinLatitude(), env.getMaxLatitude() ); return myBBox.intersects( box ); } return true; } } public class LayerNameFilter implements HistoryFilter{ String myLayer; public void initialize(String param){ myLayer = param; } public boolean pass(SyndEntry entry){ try{ String xmlBlob = ((SyndContent)entry.getContents().get(0)).getValue(); Parser parser = new Parser(xmlConfiguration); TransactionType tx = (TransactionType) parser.parse(new StringReader(xmlBlob)); Set qnames = new TreeSet(); Iterator it = tx.getInsert().iterator(); while (it.hasNext()){ InsertElementType iet = (InsertElementType)it.next(); Iterator iter = iet.getFeature().iterator(); while (iter.hasNext()){ qnames.add(((SimpleFeature)iter.next()).getFeatureType().getTypeName()); } } it = tx.getDelete().iterator(); while (it.hasNext()){ DeleteElementType det = (DeleteElementType)it.next(); qnames.add(det.getTypeName().getLocalPart()); } it = tx.getUpdate().iterator(); while (it.hasNext()){ UpdateElementType uet = (UpdateElementType)it.next(); qnames.add(uet.getTypeName().getLocalPart()); } return qnames.contains(myLayer);// evt.getLayerName().toString()); } catch (Exception e){ e.printStackTrace(); // pass since we didn't understand, I guess // TODO: revisit and decide whether unparsable stuff should stay in the feed return true; } } } }