package org.waveprotocol.box.server.persistence.mongodb;
import org.waveprotocol.box.common.comms.WaveClientRpc.WaveletSnapshot;
import org.waveprotocol.box.server.common.SnapshotSerializer;
import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.box.server.waveserver.DeltaStore;
import org.waveprotocol.wave.model.id.InvalidIdException;
import org.waveprotocol.wave.model.id.ModernIdSerialiser;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.wave.InvalidParticipantAddress;
import org.waveprotocol.wave.model.wave.data.ReadableWaveletData;
import org.waveprotocol.wave.model.wave.data.WaveletData;
import org.waveprotocol.wave.util.logging.Log;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
import com.mongodb.WriteConcern;
/**
* A MongoDB-backed store of Wavelet snapshots.
* The aim is to avoid the whole processing of deltas when a
* wavelet is loaded into server's memory for the first time.
*
* This class is not thread-safe.
*
* @author pablojan@gmail.com (Pablo Ojanguren)
*
*/
public class MongoDBSnapshotStore {
private static final Log LOG = Log.get(MongoDBSnapshotStore.class);
protected static final String WAVE_ID_FIELD = "waveid";
protected static final String WAVELET_ID_FIELD = "waveletid";
protected static final String VERSION_FIELD = "version";
protected static final String VERSION_HASH_FIELD = "versionhash";
protected static final String LASTMOD_FIELD = "lastmod";
protected static final String SNAPSHOT_DATA = "data";
/** Name of the MongoDB collection to store Deltas */
protected static final String SNAPSHOT_COLLECTION = "snapshots";
private final DBCollection collection;
/**
* Get a reference to the snapshots store.
*
* @param database
* @return
*/
public static MongoDBSnapshotStore create(DB database) {
Preconditions.checkArgument(database != null, "Unable to get reference to mongoDB snapshots collection");
DBCollection collection = database.getCollection(SNAPSHOT_COLLECTION);
return new MongoDBSnapshotStore(collection);
}
/**
* Construct a new snapshots store.
*
* @param database the database connection object
*/
protected MongoDBSnapshotStore(DBCollection collection) {
this.collection = collection;
}
protected void deleteSnapshot(WaveletName waveletName) throws PersistenceException {
DBObject criteria = new BasicDBObject();
criteria.put(WAVE_ID_FIELD, ModernIdSerialiser.INSTANCE.serialiseWaveId(waveletName.waveId));
criteria.put(WAVELET_ID_FIELD, ModernIdSerialiser.INSTANCE.serialiseWaveletId(waveletName.waveletId));
try {
// Using Journaled Write Concern
// (http://docs.mongodb.org/manual/core/write-concern/#journaled)
collection.remove(criteria, WriteConcern.JOURNALED);
} catch (MongoException e) {
throw new PersistenceException(e);
}
}
/**
* Store a snapshot
*
* @param waveletData
* @param hashedVersion
* @throws PersistenceException
*/
public void store(ReadableWaveletData waveletData) throws PersistenceException {
String waveId = ModernIdSerialiser.INSTANCE.serialiseWaveId(waveletData.getWaveId());
String waveletId = ModernIdSerialiser.INSTANCE.serialiseWaveletId(waveletData.getWaveletId());
// store new snapshot
BasicDBObject dbo = new BasicDBObject();
dbo.put(WAVE_ID_FIELD, ModernIdSerialiser.INSTANCE.serialiseWaveId(waveletData.getWaveId()));
dbo.put(WAVELET_ID_FIELD, ModernIdSerialiser.INSTANCE.serialiseWaveletId(waveletData.getWaveletId()));
dbo.put(VERSION_FIELD, waveletData.getHashedVersion().getVersion());
dbo.put(VERSION_HASH_FIELD, waveletData.getHashedVersion().getHistoryHash());
dbo.put(LASTMOD_FIELD,waveletData.getLastModifiedTime());
WaveletSnapshot snapshot = SnapshotSerializer.serializeWavelet(waveletData, waveletData.getHashedVersion());
dbo.put(SNAPSHOT_DATA,snapshot.toByteArray());
try {
collection.insert(dbo);
} catch (MongoException e) {
LOG.warning("Error storing wavelet snapshot for "+waveId+"/"+waveletId, e);
throw new PersistenceException(e);
}
// Delete previous snapshots of this wavelet
DBObject query = new BasicDBObject();
query.put(WAVE_ID_FIELD, waveId);
query.put(WAVELET_ID_FIELD, waveletId);
query.put(VERSION_FIELD, new BasicDBObject("$lt", waveletData.getHashedVersion().getVersion()));
try {
collection.remove(query);
} catch (MongoException e) {
LOG.warning("Error deleting outdated wavelet snapshots for "+waveId+"/"+waveletId, e);
throw new PersistenceException(e);
}
LOG.fine("Stored snaphost for "+waveId+"/"+waveletId+" version "+ waveletData.getHashedVersion().getVersion());
}
public DeltaStore.Snapshot load(WaveletName waveletName) throws PersistenceException {
String waveId = ModernIdSerialiser.INSTANCE.serialiseWaveId(waveletName.waveId);
String waveletId = ModernIdSerialiser.INSTANCE.serialiseWaveletId(waveletName.waveletId);
// find last snapshot stored
DBObject query = new BasicDBObject();
query.put(WAVE_ID_FIELD, waveId);
query.put(WAVELET_ID_FIELD, waveletId);
DBObject snapshotDBObject = null;
try {
snapshotDBObject = collection.findOne(query);
} catch (MongoException e) {
LOG.warning("Error querying wavelet snapshots for "+waveId+"/"+waveletId, e);
throw new PersistenceException(e);
}
if (snapshotDBObject == null)
return null;
WaveletSnapshot snapshot = null;
try {
snapshot = WaveletSnapshot.parseFrom((byte[]) snapshotDBObject.get(SNAPSHOT_DATA));
} catch (InvalidProtocolBufferException e) {
throw new PersistenceException(e);
}
try {
final WaveletData waveletData = SnapshotSerializer.deserializeWavelet(snapshot, waveletName.waveId);
return new DeltaStore.Snapshot() {
@Override
public WaveletData getWaveletData() {
return waveletData;
}
};
} catch (OperationException e) {
throw new PersistenceException(e);
} catch (InvalidParticipantAddress e) {
throw new PersistenceException(e);
} catch (InvalidIdException e) {
throw new PersistenceException(e);
}
}
}