/* Copyright (c) 2013-2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * David Winslow (Boundless) - initial implementation */ package org.locationtech.geogig.storage.mongo; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.repository.RepositoryConnectionException; import org.locationtech.geogig.storage.ConfigDatabase; import org.locationtech.geogig.storage.GraphDatabase; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.inject.Inject; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.mongodb.MongoClient; /** * A graph database that uses a MongoDB server for persistence. */ public class MongoGraphDatabase implements GraphDatabase { private final MongoConnectionManager manager; private final ConfigDatabase config; private MongoClient client; private DBCollection collection; @Inject public MongoGraphDatabase(final MongoConnectionManager manager, final ConfigDatabase config) { this.config = config; this.manager = manager; } @Override public void configure() throws RepositoryConnectionException { RepositoryConnectionException.StorageType.GRAPH.configure(config, "mongodb", "0.1"); } @Override public void checkConfig() throws RepositoryConnectionException { RepositoryConnectionException.StorageType.GRAPH.verify(config, "mongodb", "0.1"); } private class MongoNode extends GraphNode { private DBObject dbObject; public MongoNode(DBObject dbObject) { this.dbObject = dbObject; } @Override public boolean isSparse() { DBObject properties = (DBObject) dbObject.get("_properties"); return Boolean.valueOf((String) properties.get(SPARSE_FLAG)); } @Override public ObjectId getIdentifier() { DBObject properties = (DBObject) dbObject.get("_properties"); return ObjectId.valueOf((String) properties.get("identifier")); } @Override public Iterator<GraphEdge> getEdges(final Direction direction) { DBObject properties = (DBObject) dbObject.get("_properties"); final String id = (String) properties.get("identifier"); DBObject query = new BasicDBObject(); switch (direction) { case OUT: query.put("_in", id); break; case IN: query.put("_out", id); break; case BOTH: DBObject in = new BasicDBObject("_in", id); DBObject out = new BasicDBObject("_out", id); query.put("$or", new DBObject[]{ in, out }); break; default: throw new IllegalStateException("Unexpected direction value"); } DBCursor cursor = collection.find(query); Function<DBObject, GraphEdge> mapper = new Function<DBObject, GraphEdge>() { @Override public GraphEdge apply(DBObject dbObject) { GraphNode in, out; if (id.equals(dbObject.get("_out"))) { in = getNode(ObjectId.valueOf((String)dbObject.get("_in"))); out = MongoNode.this; } else { out = getNode(ObjectId.valueOf((String)dbObject.get("_out"))); in = MongoNode.this; } return new GraphEdge(in, out); } }; return Iterators.transform(cursor.iterator(), mapper); } @Override public String toString() { return getIdentifier().toString(); } } @Override public void open() { String uri = config.get("mongodb.uri").get(); String database = config.get("mongodb.database").get(); this.client = manager.acquire(new MongoAddress(uri)); DB db = client.getDB(database); this.collection = db.getCollection("graph"); } @Override public boolean isOpen() { return this.client != null; } @Override public void close() { this.client.close(); this.client = null; this.collection = null; } @Override public boolean exists(ObjectId id) { DBObject query = idQuery(id); DBObject result = collection.findOne(query); return result != null; } private DBObject idQuery(ObjectId id) { DBObject query = new BasicDBObject("_properties.identifier", id.toString()); return query; } @Override public void map(ObjectId id, ObjectId mappedId) { DBObject query = new BasicDBObject(); query.put("_label", Relationship.MAPPED_TO.name()); query.put("_out", id.toString()); DBObject edge = collection.findOne(query); if (edge != null) { edge.put("_in", mappedId.toString()); collection.save(edge); } else { edge = new BasicDBObject(); edge.put("_label", Relationship.MAPPED_TO.name()); edge.put("_out", id.toString()); edge.put("_in", mappedId.toString()); collection.insert(edge); } } @Override public boolean put(ObjectId id, ImmutableList<ObjectId> ids) { DBObject query = idQuery(id); DBObject result = collection.findOne(query); if (result != null) { return false; } else { DBObject record = new BasicDBObject(); record.put("_properties", new BasicDBObject("identifier", id.toString())); collection.insert(record); for (ObjectId parent : ids) { DBObject edge = new BasicDBObject(); edge.put("_label", Relationship.PARENT.name()); edge.put("_in", id.toString()); edge.put("_out", parent.toString()); collection.insert(edge); } return true; } } @Override public ImmutableList<ObjectId> getChildren(ObjectId id) { DBObject query = new BasicDBObject(); query.put("_label", Relationship.PARENT.name()); query.put("_out", id.toString()); DBCursor cursor = collection.find(query); Function<DBObject, ObjectId> idMapper = new Function<DBObject, ObjectId>() { @Override public ObjectId apply(DBObject o) { return ObjectId.valueOf((String)o.get("_in")); } }; return ImmutableList.copyOf(Iterators.transform(cursor.iterator(), idMapper)); } @Override public int getDepth(ObjectId id) { int depth = 0; Set<ObjectId> front = new HashSet<ObjectId>(); front.add(id); Function<ObjectId, String> idMapper = new Function<ObjectId, String>() { @Override public String apply(ObjectId id) { return id.toString(); } }; while (front.size() > 0) { DBObject query = new BasicDBObject(); query.put("_label", Relationship.PARENT.name()); query.put("_in", new BasicDBObject("$in", Lists.transform(new ArrayList<ObjectId>(front), idMapper))); DBCursor result = collection.find(query); Set<ObjectId> nextFront = new HashSet<ObjectId>(); for (DBObject o : result) { front.remove(ObjectId.valueOf((String) o.get("_in"))); nextFront.add(ObjectId.valueOf((String) o.get("_out"))); } if (front.size() > 0) { break; } else { front = nextFront; depth += 1; } } return depth; } @Override public ObjectId getMapping(ObjectId mappedId) { DBObject query = new BasicDBObject(); query.put("_out", mappedId.toString()); query.put("_label", Relationship.MAPPED_TO.name()); DBObject result = collection.findOne(query); if (result == null) return null; return ObjectId.valueOf((String) result.get("_in")); } @Override public GraphNode getNode(ObjectId id) { DBObject query = idQuery(id); DBObject result = collection.findOne(query); if (result == null) throw new RuntimeException("No such node: " + id); return new MongoNode(result); } @Override public ImmutableList<ObjectId> getParents(ObjectId id) { DBObject query = new BasicDBObject(); query.put("_label", Relationship.PARENT.name()); query.put("_in", id.toString()); DBCursor cursor = collection.find(query); Function<DBObject, ObjectId> idMapper = new Function<DBObject, ObjectId>() { @Override public ObjectId apply(DBObject o) { return ObjectId.valueOf((String)o.get("_out")); } }; return ImmutableList.copyOf(Iterators.transform(cursor.iterator(), idMapper)); } @Override public void setProperty(ObjectId id, String name, String value) { DBObject query = idQuery(id); DBObject record = collection.findOne(query); DBObject properties = (DBObject) record.get("_properties"); properties.put(name, value); record.put("_properties", properties); collection.save(record); } @Override public void truncate() { // NO-OP } @Override public String toString() { return String.format("%s[uri: %s]", getClass().getSimpleName(), config == null ? "<unknown>" : config.get("mongodb.uri").or("<unset>")); } }