Java tutorial
package org.geotools.data.mongodb; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.logging.Logger; import org.bson.BSONObject; import org.bson.BasicBSONObject; import org.bson.types.BasicBSONList; import org.geotools.feature.AttributeTypeBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.Bytes; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * Represents a GeoServer layer consisting of valid GeoJSON-encoded data from a mongoDB collection. * (A single collection containing different geometry types (Point, Polygon etc.) may be represented * by multiple layers.) * * @author Gerald Gay, Data Tactics Corp. * @author Alan Mangan, Data Tactics Corp. * @source $URL$ * * (C) 2011, Open Source Geospatial Foundation (OSGeo) * * @see The GNU Lesser General Public License (LGPL) */ /* This library 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 library 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. * * You should have received a copy of the GNU Lesser General Public License along with this library; * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ public class MongoLayer { private MongoPluginConfig config = null; private String layerName = null; private SimpleFeatureType schema = null; private Set<String> keywords = null; private CoordinateReferenceSystem crs = null; /** meta data for layer defining geometry type, property field names and types */ private DBObject metaData = null; /** Supported GeoJSON geometry types */ static public enum GeometryType { GeometryCollection, LineString, Point, Polygon, MultiLineString, MultiPoint, MultiPolygon, Unknown; } /** Geometry type for this layer */ private GeometryType geometryType = null; /** * How to calculate collection record fields and types Majority: for same named fields with * different types use major instance to determine which type to assign String: if same named * fields with different types exist; store them as Strings */ static public enum RecordBuilder { MAJORITY, STRING; } /** How to build records with potentially different types for this layer */ private RecordBuilder buildRule = RecordBuilder.MAJORITY; /** Metadata map function (ensure no comments) */ private String metaMapFunc = "function() { mapfields_recursive (\"\", this);}"; /** Metadata reduce function (ensure no comments) */ private String metaReduceFunc = "function (key, vals) {" + " sum = 0;" + " for (var i in vals) sum += vals[i];" + " return sum;" + "}"; /** Name of collection holding metadata results */ private String metaResultsColl = "FieldsAndTypes"; /** * Mapping from class string names from mongo map-reduce to corresponding Java Class NB needs to * be synced with MetaDataCompute.js javascript file */ static private HashMap<String, String> classNameMap = new HashMap<String, String>(); static { classNameMap.put("array", BasicDBList.class.getCanonicalName()); classNameMap.put("boolean", Boolean.class.getCanonicalName()); classNameMap.put("date", Date.class.getCanonicalName()); classNameMap.put("double", Double.class.getCanonicalName()); classNameMap.put("long", Long.class.getCanonicalName()); classNameMap.put("object", BasicDBObject.class.getCanonicalName()); classNameMap.put("string", String.class.getCanonicalName()); } /** Package logger */ static private final Logger log = MongoPluginConfig.getLog(); public MongoLayer(DBCollection coll, MongoPluginConfig config) { this.config = config; layerName = coll.getName(); log.fine("MongoLayer; layerName " + layerName); keywords = new HashSet<String>(); SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); builder.setName(layerName); builder.setNamespaceURI(config.getNamespace()); // Always add _id... AttributeTypeBuilder b = new AttributeTypeBuilder(); b.setBinding(String.class); b.setName("_id"); b.setNillable(false); b.setDefaultValue(null); b.setLength(1024); AttributeDescriptor a = b.buildDescriptor("_id"); builder.add(a); // We could get this out of the table, exercise for the reader... TODO try { crs = CRS.decode("EPSG:4326"); } catch (Throwable t) { crs = DefaultGeographicCRS.WGS84; } b = new AttributeTypeBuilder(); b.setName("geometry"); b.setNillable(false); b.setDefaultValue(null); b.setCRS(crs); // determine metadata for this collection metaData = getCollectionModel(coll, buildRule); // determine geometry type setGeometryType(metaData); switch (geometryType) { case GeometryCollection: b.setBinding(GeometryCollection.class); break; case LineString: b.setBinding(LineString.class); break; case Point: b.setBinding(Point.class); break; case Polygon: b.setBinding(Polygon.class); break; case MultiLineString: b.setBinding(MultiLineString.class); break; case MultiPoint: b.setBinding(MultiPoint.class); break; case MultiPolygon: b.setBinding(MultiPolygon.class); break; case Unknown: log.warning("Unknown geometry for layer " + layerName + " (but has valid distinct geometry.type)"); return; } a = b.buildDescriptor("geometry"); builder.add(a); // Add the 2 known keywords... keywords.add("_id"); keywords.add("geometry"); // Now get all the properties... DBObject props = (DBObject) metaData.get("properties"); addAttributes(builder, props, "properties"); schema = builder.buildFeatureType(); } /** * Add JSON attributes to geo tools schema * * @param builder geo tools feature builder * @param dbo base object; either a JSON object or Array * @param baseProp base property name, e.g. "properties" or nested properties objects and/or * arrays */ private void addAttributes(SimpleFeatureTypeBuilder builder, BSONObject dbo, String baseProp) { Set<String> cols = dbo.keySet(); for (String col : cols) { Object dbcol = dbo.get(col); String propName = baseProp + "." + col; keywords.add(propName); // cannot bind to nulls; only handle non-nulls if (dbcol != null) { // handle as native types AttributeTypeBuilder b = new AttributeTypeBuilder(); b.setName(propName); b.setBinding(dbcol.getClass()); b.setNillable(true); b.setDefaultValue(null); b.setLength(1024); AttributeDescriptor a = b.buildDescriptor(propName); builder.add(a); // add attrs for nested JSON or Array objects if (dbcol instanceof BasicDBObject || dbcol instanceof BasicBSONList) { addAttributes(builder, (BSONObject) dbcol, propName); } } } } public String getName() { return layerName; } public SimpleFeatureType getSchema() { return schema; } public Set<String> getKeywords() { return keywords; } public CoordinateReferenceSystem getCRS() { return crs; } public MongoPluginConfig getConfig() { return config; } /** * Get GeometryType * * @return GeometryType, may be null if not set to valid and supported GeoJSON geometry */ public GeometryType getGeometryType() { return geometryType; } /** * Generate model of collection records' data fields and types * * @param coll mongo collection * @param buildRule which rule to apply if same named fields with different types exist * @return JSON object describing collection record */ private DBObject getCollectionModel(DBCollection coll, RecordBuilder buildRule) { // call map-reduce job to generate metadata // mongo java driver calls mapReduce with the functions rather than the name of the // functions // function prototypes from scripts/mrscripts/MetaDataCompute.js // (do not include comments in quoted javascript functions below-gives mongo error) coll.mapReduce(metaMapFunc, metaReduceFunc, metaResultsColl, new BasicDBObject()); // get mapping of field names and types, and counts for different types DBCollection metaColl = coll.getDB().getCollection(metaResultsColl); HashMap<String, ClassCount> fieldMap = getFieldMap(metaColl); log.finest("fieldMap=" + fieldMap); // resulting collection may have dupes for fields of different types // use build rule to determine final type HashMap<String, String> finalMap = finalizeMajorityRule(fieldMap, buildRule); log.finest("finalMap=" + finalMap); // convert map of field names with types and associated counts to a JSON DBObject DBObject metaData = convertMapToJson(finalMap); log.finest("metaData=" + metaData); return metaData; } /** * Get mapping of field names and types, and counts for different types * * @param collection where metadata from map-reduce job stored, in format: { "_id" : { * "fieldname" : "geometry.type", "type" : "Point"}, "value" : 2 } { "_id" : { * "fieldname" : "properties.ActivityDescription", "type" : "number" }, "value" : 1 } * { "_id" : { "fieldname" : "properties.ActivityDescription", "type" : "string" }, * "value" : 3 } where value is number of occurrences for given type * @return mapping of field names to ClassCount holding type and count info */ private HashMap<String, ClassCount> getFieldMap(DBCollection metaResultsColl) { // cursor over collection BasicDBObject query = new BasicDBObject(); DBCursor cursor = metaResultsColl.find(query); // avoid cursor timeout cursor.addOption(Bytes.QUERYOPTION_NOTIMEOUT); // map to store fieldname and ClasCount object holding type and type-count info HashMap<String, ClassCount> fieldMap = new HashMap<String, ClassCount>(); try { // iterate over each record while (cursor.hasNext()) { // check type found for current field DBObject currRec = cursor.next(); DBObject currField = (DBObject) currRec.get("_id"); String fieldName = (String) currField.get("fieldname"); String fieldType = (String) currField.get("type"); int typeCount = ((Double) currRec.get("value")).intValue(); // if first occurrence of field name instantiate counter if (!fieldMap.containsKey(fieldName)) { fieldMap.put(fieldName, new ClassCount(fieldType, typeCount)); } // else increment count for given type else { ClassCount currCount = fieldMap.get(fieldName); currCount.add(fieldType, typeCount); fieldMap.put(fieldName, currCount); } } } finally { // need to explicitly release cursor since notimeout option set cursor.close(); } return fieldMap; } /** * Apply build rule to determine final type * * @param fieldMap map holding field name and type data * @param buildRule build rule to apply; convert conflicts to String, use * @return mapping of field names to Java Classes */ private HashMap<String, String> finalizeMajorityRule(HashMap<String, ClassCount> fieldMap, RecordBuilder buildRule) { HashMap<String, String> finalMap = new HashMap<String, String>(); for (String field : fieldMap.keySet()) { String finalClass = fieldMap.get(field).getMajorityClass(field, buildRule); finalMap.put(field, finalClass); } return finalMap; } /** * Convert map of field names and Java Classes to JSON representation * * @param finalMap map with field names and types * @return metadata GeoJSON representation as DBObject */ private DBObject convertMapToJson(HashMap<String, String> finalMap) { BasicDBObject metaData = new BasicDBObject(); // add geometry type BasicDBObject geometry = new BasicDBObject(); geometry.append("type", finalMap.get("geometry.type")); metaData.append("geometry", geometry); // add properties BasicDBObject properties = new BasicDBObject(); properties = (BasicDBObject) recreateJson("properties", properties, finalMap); metaData.append("properties", properties); return metaData; } /** * Build JSON object from map of property names and types * * @param baseName base name, e.g. "properties" * @param base base object to store results, either BasicDBObject or BasicBSONList * @param fieldMap map of Java Class names indexed by field name * @return BSONObject object, either JSON or Array */ private BSONObject recreateJson(String baseName, Object base, HashMap<String, String> fieldMap) { // strip relevant field names corresponding to required property HashMap<String, String> propMap = new HashMap<String, String>(); for (String key : fieldMap.keySet()) { if (key.startsWith(baseName + ".")) { String propKey = key.substring(baseName.length() + 1); propMap.put(propKey, fieldMap.get(key)); } } // convert propMap to appropriate object; either BasicDBObject (JSON) or BasicBSONList // (Array) BSONObject json = null; if (base instanceof BasicDBObject) { json = new BasicDBObject(); } else if (base instanceof BasicBSONList) { json = new BasicBSONList(); } else { log.warning("Error, can only process BasicDBObject (JSON) or BasicBSONList (Array), base is a " + base.getClass()); return new BasicBSONObject(); } for (String propKey : propMap.keySet()) { if (!propKey.contains(".")) { // ignore nulls if (propMap.get(propKey) != null) { // check for nested JSON or Array (BasicDBObject or BasicBSONList) if (propMap.get(propKey).equals("com.mongodb.BasicDBObject")) { BasicDBObject subJSON = new BasicDBObject(); json.put(propKey, recreateJson(propKey, subJSON, propMap)); } else if (propMap.get(propKey).equals("com.mongodb.BasicDBList")) { BasicBSONList subArray = new BasicBSONList(); json.put(propKey, recreateJson(propKey, subArray, propMap)); } else { try { json.put(propKey, Class.forName(propMap.get(propKey)).newInstance()); } catch (InstantiationException ie) { // Number subclasses no nullary cons; use constructor that takes String // arg. try { json.put(propKey, (Class.forName(propMap.get(propKey)).getConstructor(String.class)) .newInstance("0")); } catch (Exception e) { } } catch (Exception e) { } } } } } return json; } /** * Simple object to keep count of Classes and associated counts when merging existing metadata * with new, incoming metadata from collection's current record * * @author Alan Mangan */ private class ClassCount { /** Map to track counts of given classes */ private HashMap<String, Integer> classMap = new HashMap<String, Integer>(); /** * Initialize ClassCount with given initial Class * * @param initClass */ public ClassCount(String initClass, int initCount) { classMap.put(initClass, initCount); } /** * Add count for given Class * * @param newClass */ public void add(String newClass, int newCount) { if (classMap.containsKey(newClass)) { int currCount = classMap.get(newClass); classMap.put(newClass, currCount + newCount); } else { classMap.put(newClass, newCount); } } /** * Return name of Class with max number occurrences * * @param propKey original property key, "geometry.type" needs special handling * @param buildRule build rule to apply; use majority, or convert all to Strings * @return name of Class with max occurrences */ public String getMajorityClass(String propKey, RecordBuilder buildRule) { int max = -1; String maxClass = null; Set<String> keys = classMap.keySet(); // if more than one type, and build rule is String just return String type if (keys.size() > 1 && buildRule.equals(RecordBuilder.STRING) && !keys.contains("geometry.type")) { maxClass = String.class.getCanonicalName(); } else { for (String currClass : keys) { if (classMap.get(currClass) > max) { max = classMap.get(currClass); maxClass = currClass; } } // determine class for "normal" property, preserve actual type (Point etc.) for // geometry.type if (!propKey.equals("geometry.type")) { maxClass = classNameMap.get(maxClass); } } return maxClass; } @Override public String toString() { return classMap.toString(); } } /** * Set geo type for this layer based on metadata JSON obj (GeometryType.Unknown if cannot * determine) * * @param metaData JSON object with geometry.type defined */ private void setGeometryType(DBObject metaData) { try { // determine geometry type String geoTypeStr = (String) ((DBObject) metaData.get("geometry")).get("type"); geometryType = GeometryType.valueOf(geoTypeStr); } catch (Throwable t) { geometryType = GeometryType.Unknown; } } }