package org.argeo.geotools.jcr; import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import javax.jcr.Node; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.observation.Event; import javax.jcr.observation.EventIterator; import javax.jcr.observation.EventListener; import javax.jcr.observation.ObservationManager; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.ArgeoException; import org.argeo.geotools.GeoToolsUtils; import org.argeo.jcr.JcrUtils; import org.argeo.jcr.gis.GisNames; import org.argeo.jcr.gis.GisTypes; import org.argeo.jts.jcr.JtsJcrUtils; import org.geotools.data.DataStore; import org.geotools.data.DefaultTransaction; import org.geotools.data.FeatureStore; import org.geotools.data.Transaction; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureCollections; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.filter.FilterFactoryImpl; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.FilterFactory2; import org.opengis.filter.identity.FeatureId; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; public class GeoJcrIndex implements EventListener, GisNames, GisTypes { // PostGIS convention final static String DEFAULT_GEOM_NAME = "the_geom"; private final static Log log = LogFactory.getLog(GeoJcrIndex.class); private DataStore dataStore; private Session session; private Executor systemExecutionService; /** The key is the workspace */ private Map> geoJcrIndexes = Collections .synchronizedMap(new HashMap>()); // TODO: use common factory finder? private FilterFactory2 ff = new FilterFactoryImpl(); public void init() { systemExecutionService.execute(new Runnable() { public void run() { initGeoJcrIndex(); } }); } protected void initGeoJcrIndex() { try { // create GIS schema SimpleFeatureType indexType = getWorkspaceGeoJcrIndexType(session .getWorkspace().getName()); GeoToolsUtils.createSchemaIfNeeded(dataStore, indexType); // register JCR listeners ObservationManager om = session.getWorkspace() .getObservationManager(); om.addEventListener(this, Event.NODE_ADDED | Event.NODE_REMOVED, "/", true, null, null, false); // FIXME: use a different listener for properties since it resets // the filter // om.addEventListener(this, Event.PROPERTY_CHANGED, "/", // true, null, new String[] { GIS_LOCATED }, false); } catch (RepositoryException e) { throw new ArgeoException("Cannot initialize GeoJcr index", e); } } public void dispose() { } public void onEvent(final EventIterator events) { final FeatureCollection toAdd = FeatureCollections .newCollection(); final Set toRemove = new HashSet(); // execute with system authentication so that JCR can be read systemExecutionService.execute(new Runnable() { public void run() { while (events.hasNext()) { Event event = events.nextEvent(); try { Integer eventType = event.getType(); if (Event.NODE_ADDED == eventType) { Node node = session.getNodeByIdentifier(event .getIdentifier()); if (node.isNodeType(GIS_INDEXED)) { SimpleFeature feature = mapNodeToFeature(node, getGeoJcrIndex().getSchema()); toAdd.add(feature); } } else if (Event.NODE_REMOVED == eventType) { // we have no way to check whether the node was // actually // geoindexed without querying the index, this is // therefore // more optimal to create a filter with all ideas // and apply // a remove later on String id = event.getIdentifier(); toRemove.add(ff.featureId(id)); } else if (Event.PROPERTY_CHANGED == eventType) { // TODO: monitor changes to SRS, BBOX AND CENTROID } } catch (Exception e) { log.error("Cannot process event " + event, e); } } } }); // TODO: this may be more optimal to persist in one single // transaction, // but we will loose modifications on all nodes if a single one // fails try { Transaction transaction = new DefaultTransaction(); getGeoJcrIndex().setTransaction(transaction); try { // points getGeoJcrIndex().addFeatures(toAdd); if (toRemove.size() != 0) getGeoJcrIndex().removeFeatures(ff.id(toRemove)); transaction.commit(); } catch (Exception e) { transaction.rollback(); throw new ArgeoException("Cannot persist changes", e); } finally { transaction.close(); } } catch (ArgeoException e) { throw e; } catch (IOException e) { throw new ArgeoException("Unexpected issue with the transaction", e); } } protected FeatureStore getGeoJcrIndex() { String workspaceName = session.getWorkspace().getName(); if (!geoJcrIndexes.containsKey(workspaceName)) { SimpleFeatureType indexType = getWorkspaceGeoJcrIndexType(workspaceName); FeatureStore geoIndex = GeoToolsUtils .getFeatureStore(dataStore, indexType.getName()); geoJcrIndexes.put(workspaceName, geoIndex); } return geoJcrIndexes.get(workspaceName); } protected SimpleFeatureType getWorkspaceGeoJcrIndexType(String workspaceName) { SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); builder.setNamespaceURI(GIS_NAMESPACE); builder.setName(workspaceName + "_geojcr_index"); builder.setDefaultGeometry(JcrUtils.normalize(GIS_BBOX)); builder.add(JcrUtils.normalize(GIS_BBOX), Polygon.class); builder.add(JcrUtils.normalize(GIS_CENTROID), Point.class); builder.add(JcrUtils.normalize("jcr:uuid"), String.class); builder.add(JcrUtils.normalize("jcr:path"), String.class); builder.add(JcrUtils.normalize("jcr:primaryType"), String.class); // mix:lastModified builder.add(JcrUtils.normalize("jcr:lastModified"), Date.class); builder.add(JcrUtils.normalize("jcr:lastModifiedBy"), String.class); return builder.buildFeatureType(); } protected SimpleFeature mapNodeToFeature(Node node, SimpleFeatureType type) { try { SimpleFeatureBuilder builder = new SimpleFeatureBuilder(type); Node locatedNode; if (node.isNodeType(GIS_LOCATED)) { locatedNode = node; } else if (node.isNodeType(GIS_INDEXED)) { locatedNode = findLocatedParent(node); } else { throw new ArgeoException("Unsupported node " + node); } // TODO: reproject to the feature store SRS Polygon bbox = (Polygon) JtsJcrUtils.readWkb(locatedNode .getProperty(GIS_BBOX)); builder.set(JcrUtils.normalize(GIS_BBOX), bbox); Point centroid = (Point) JtsJcrUtils.readWkb(locatedNode .getProperty(GIS_CENTROID)); builder.set(JcrUtils.normalize(GIS_CENTROID), centroid); builder.set(JcrUtils.normalize("jcr:uuid"), node.getIdentifier()); builder.set(JcrUtils.normalize("jcr:path"), node.getPath()); builder.set(JcrUtils.normalize("jcr:primaryType"), node .getPrimaryNodeType().getName()); if (node.hasProperty(Property.JCR_LAST_MODIFIED)) builder.set(JcrUtils.normalize("jcr:lastModified"), node .getProperty(Property.JCR_LAST_MODIFIED).getDate() .getTime()); if (node.hasProperty(Property.JCR_LAST_MODIFIED_BY)) builder.set(JcrUtils.normalize("jcr:lastModifiedBy"), node .getProperty(Property.JCR_LAST_MODIFIED_BY).getString()); return builder.buildFeature(node.getIdentifier()); } catch (RepositoryException e) { throw new ArgeoException("Cannot map " + node + " to " + type, e); } } protected Node findLocatedParent(Node child) { try { if (child.getParent().isNodeType(GIS_LOCATED)) return child.getParent(); else return findLocatedParent(child.getParent()); } catch (Exception e) { // also if child is root node throw new ArgeoException("Cannot find located parent", e); } } /** Returns the node as a point in the CRS of the related feature store. */ protected Geometry reproject(CoordinateReferenceSystem crs, Geometry geometry, CoordinateReferenceSystem targetCrs) { // transform if not same CRS // FIXME: there is certainly a more standard way to reproject if (!targetCrs.getIdentifiers().contains(crs.getName())) { throw new ArgeoException("Reprojection not yet supported"); // MathTransform transform; // try { // transform = CRS.findMathTransform(nodeCrs, featureStoreCrs); // if (geometry instanceof Point) { // Point point = (Point) geometry; // DirectPosition2D pos = new DirectPosition2D(nodeCrs, // point.getX(), point.getY()); // DirectPosition targetPos = transform.transform(pos, null); // return geometryFactory.createPoint(new Coordinate(targetPos // .getCoordinate()[0], targetPos.getCoordinate()[1])); // } else if (geometry instanceof Polygon) { // Polygon polygon = (Polygon) geometry; // List coordinates = new ArrayList(); // for (Coordinate coo : polygon.getExteriorRing()) { // DirectPosition pos = new DirectPosition2D(nodeCrs, // coo.x, coo.y); // DirectPosition targetPos = transform.transform(pos, // null); // // coordinates.add(o) // } // LinearRing ring = geometryFactory // .createLinearRing(coordinates // .toArray(new Coordinate[coordinates.size()])); // return geometryFactory.createPolygon(ring, null); // } // } catch (Exception e) { // throw new ArgeoException("Cannot transform from " + nodeCrs // + " to " + featureStoreCrs, e); // } } else { return geometry; } } public void setDataStore(DataStore dataStore) { this.dataStore = dataStore; } public void setSession(Session session) { this.session = session; } public void setSystemExecutionService( Executor systemExecutionService) { this.systemExecutionService = systemExecutionService; } }