/** * Copyright (c) 2011-2012, Thilo Planz. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package v7db.files.mongodb; import java.util.ArrayList; import java.util.List; import org.bson.BSONObject; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBObject; import com.mongodb.WriteConcern; import com.mongodb.WriteResult; class Vermongo { static final String _VERSION = "_version"; /** * inserts a new object into the collection. The _version property must not * be present in the object, and will be set to 1 (integer). * * @param object */ static void insert(DBCollection collection, DBObject object) { if (object.containsField(_VERSION)) throw new IllegalArgumentException(); object.put(_VERSION, 1); collection.insert(object, WriteConcern.SAFE); } /** * updates an existing object. The object must have been the _version * property set to the version number of the base revision (i.e. the version * number to be replaced). If the current version in the DB does not have a * matching version number, the operation aborts with an * UpdateConflictException. * * After the update is successful, _version in the object is updated to the * new version number. * * The version that was replaced is moved into the collection's shadow * collection. * * @param collection * @param object * @throws UpdateConflictException */ static void update(DBCollection collection, DBObject object) throws UpdateConflictException { if (!object.containsField(_VERSION)) throw new IllegalArgumentException( "the base version number needs to be included as _version"); int baseVersion = (Integer) object.get(_VERSION); // load the base version { DBObject base = collection.findOne(new BasicDBObject("_id", getId(object))); if (base == null) { throw new IllegalArgumentException( "document to update not found in collection"); } Object bV = base.get(_VERSION); if (bV instanceof Integer) { if (baseVersion != (Integer) bV) { throw new UpdateConflictException(object, base); } } else { throw new UpdateConflictException(object, base); } // copy to shadow DBCollection shadow = getShadowCollection(collection); base.put("_id", new BasicDBObject("_id", getId(base)).append( _VERSION, baseVersion)); WriteResult r = shadow.insert(base, WriteConcern.SAFE); // TODO: if already there, no error r.getLastError().throwOnError(); } try { object.put(_VERSION, baseVersion + 1); DBObject found = collection.findAndModify(new BasicDBObject("_id", getId(object)).append(_VERSION, baseVersion), object); if (found == null) { // document has changed in the mean-time. get the latest version // again DBObject base = collection.findOne(new BasicDBObject("_id", getId(object))); if (base == null) { throw new IllegalArgumentException( "document to update not found in collection"); } throw new UpdateConflictException(object, base); } } catch (RuntimeException e) { object.put(_VERSION, baseVersion); throw e; } } /** * @return the _id property of the object */ static Object getId(BSONObject o) { return o.get("_id"); } /** * @return the version number of the object (from the _version property) */ static Integer getVersion(BSONObject o) { return (Integer) o.get(_VERSION); } /** * @return true, if the object represents a dummy version inserted to mark a * deleted version */ static boolean isDeletedDummyVersion(BSONObject o) { return o.get(_VERSION).toString().startsWith("deleted:"); } /** * deletes the object without checking for conflicts. An existing version is * moved to the shadow collection, along with a dummy version to mark the * deletion. This dummy version can contain optional meta-data (such as who * deleted the object, and when). */ static DBObject remove(DBCollection collection, Object id, BSONObject metaData) { DBObject base = collection.findOne(new BasicDBObject("_id", id)); if (base == null) return null; // copy to shadow DBCollection shadow = getShadowCollection(collection); int version = getVersion(base); BasicDBObject revId = new BasicDBObject("_id", getId(base)).append( _VERSION, version); base.put("_id", revId); WriteResult r = shadow.insert(base, WriteConcern.SAFE); // TODO: if already there, no error r.getLastError().throwOnError(); // add the dummy version BasicDBObject dummy = new BasicDBObject("_id", revId.append(_VERSION, version + 1)).append(_VERSION, "deleted:" + (version + 1)); if (metaData != null) dummy.putAll(metaData); r = shadow.insert(dummy, WriteConcern.SAFE); // TODO: if already there, no error r.getLastError().throwOnError(); collection.remove(new BasicDBObject("_id", id)); return base; } /** * @return the shadow collection wherein the old versions of documents are * stored */ static DBCollection getShadowCollection(DBCollection c) { return c.getCollection("vermongo"); } /** * @return an old version of the document with the given id, at the given * version number */ static DBObject getOldVersion(DBCollection c, Object id, int versionNumber) { BasicDBObject query = new BasicDBObject("_id", new BasicDBObject("_id", id).append("_version", versionNumber)); DBObject result = getShadowCollection(c).findOne(query); if (result == null) return null; result.put("_id", ((BasicDBObject) getId(result)).get("_id")); return result; } /** * The list of old versions does not include the current version of the * document, but it does include dummy entries to mark the deletion (if the * document was deleted). The list is ordered by version number. * * @return the list of old version of the document with the given id */ static List<DBObject> getOldVersions(DBCollection c, Object id) { DBObject query = QueryUtils.between("_id", new BasicDBObject("_id", id) .append("_version", 0), new BasicDBObject("_id", id).append( "_version", Integer.MAX_VALUE)); List<DBObject> result = new ArrayList<DBObject>(); for (DBObject o : getShadowCollection(c).find(query).sort( new BasicDBObject("_id", 1))) { o.put("_id", ((BasicDBObject) getId(o)).get("_id")); result.add(o); } return result; } }