//Dstl (c) Crown Copyright 2017
package uk.gov.dstl.baleen.history.mongo;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Lists;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.UpdateOptions;
import uk.gov.dstl.baleen.core.history.HistoryEvent;
import uk.gov.dstl.baleen.core.history.Recordable;
import uk.gov.dstl.baleen.core.history.RecordableHistoryEvent;
import uk.gov.dstl.baleen.core.history.impl.RecordableImpl;
import uk.gov.dstl.baleen.history.helpers.AbstractDocumentHistory;
/**
* Stores history in a Mongo collection.
*
* Note that this implementation is 'live', when you add an event it is pushed to
* the database. When you get events it performs a query. Thus no data is
* retained in memory. This is particularly important for getHistory since you
* will not want to repeatedly call get() in a loop.
*
* Each document through Baleen is stored as its own Mongo document, in a
* structure of
*
* <pre>
* {
* docId: "document id"
* entities: {
* "internalid": [
* { history event }
* ],
*
* }
*
* }
* </pre>
*
*
*
*/
public class MongoDocumentHistory extends AbstractDocumentHistory<MongoHistory> {
private static final String EVENT_TYPE = "type";
private static final String EVENT_ACTION = "msg";
private static final String EVENT_REFERRER = "ref";
private static final String EVENT_TIMESTAMP = "timestamp";
private static final String RECORDABLE = "rec";
private static final String RECORDABLE_TEXT = "text";
private static final String RECORDABLE_BEGIN = "begin";
private static final String RECORDABLE_END = "end";
private static final String RECORDABLE_TYPE = "type";
private static final String DOC_ID = "docId";
private static final String ENTITIES = "entities";
private static final Logger LOGGER = LoggerFactory
.getLogger(MongoDocumentHistory.class);
private static final String EVENT_PARAMETERS = "params";
private final MongoCollection<Document> collection;
/**
* New instance, should only be called via MongoHistory.
*
* @param history
* @param collection
* @param documentId
*/
public MongoDocumentHistory(MongoHistory history, MongoCollection<Document> collection,
String documentId) {
super(history, documentId);
this.collection = collection;
}
@Override
public void add(HistoryEvent event) {
Document insert = new Document("$push",
new Document("entities."+event.getRecordable().getInternalId(), convert(event))
);
collection.updateOne(new Document(DOC_ID, getDocumentId()), insert, new UpdateOptions().upsert(true));
}
@Override
public Collection<HistoryEvent> getAllHistory() {
return convert(collection.find(new Document(DOC_ID, getDocumentId())).first());
}
@Override
public Collection<HistoryEvent> getHistory(long recordableId) {
// Get the document, but only for specific entity
return convert(collection.find(new Document(DOC_ID,
getDocumentId())).projection(new Document(ENTITIES + "."
+ recordableId, 1)).first());
}
private Document convert(HistoryEvent event) {
return new Document(RECORDABLE, new Document()
.append(RECORDABLE_TEXT, event.getRecordable().getCoveredText())
.append(RECORDABLE_END, event.getRecordable().getEnd())
.append(RECORDABLE_BEGIN, event.getRecordable().getBegin())
.append(RECORDABLE_TYPE, event.getRecordable().getTypeName())
)
.append(EVENT_ACTION, event.getAction())
.append(EVENT_TYPE, event.getEventType())
.append(EVENT_REFERRER, event.getReferrer())
.append(EVENT_TIMESTAMP, event.getTimestamp())
.append(EVENT_PARAMETERS, event.getParameters());
}
private Collection<HistoryEvent> convert(Document doc) {
if (doc == null ||doc.get(ENTITIES)== null|| !(doc.get(ENTITIES) instanceof Document)) {
LOGGER.warn("Invalid history document");
return Collections.emptyList();
}
Document entities = (Document) doc.get(ENTITIES);
List<HistoryEvent> history = Lists.newLinkedList();
for (String entityId : entities.keySet()) {
if (entities.get(entityId) != null && entities.get(entityId) instanceof List) {
List<?> list = (List<?>) entities.get(entityId);
convertForEntity(history, entityId, list);
}
}
return history;
}
private void convertForEntity(List<HistoryEvent> history, String entityId,
List<?> list) {
for (Object o : list) {
if(o instanceof Document) {
HistoryEvent he = convert(entityId, (Document) o);
if (he != null) {
history.add(he);
}
}
}
}
private HistoryEvent convert(String entityId, Document o) {
try {
long id = Long.parseLong(entityId);
Recordable recordable = convertToRecordable(id,
(Document) o.get(RECORDABLE));
String eventType = (String) o.get(EVENT_TYPE);
String referrer = (String) o.get(EVENT_REFERRER);
String action = (String) o.get(EVENT_ACTION);
long timestamp = (long) o.get(EVENT_TIMESTAMP);
Map<String,String> params = convertToStringMap(o.get(EVENT_PARAMETERS));
return new RecordableHistoryEvent(eventType, timestamp, recordable, referrer, action, params);
} catch (Exception e) {
LOGGER.warn("Unable to deserialise history event", e);
return null;
}
}
private Map<String, String> convertToStringMap(Object object) {
if(object == null || !(object instanceof Document)) {
return ImmutableMap.of();
}
Document dbo = (Document)object;
Builder<String, String> builder = ImmutableMap.builder();
for(Entry<String,Object> e : dbo.entrySet()) {
if(e.getValue() instanceof String) {
builder.put(e.getKey(), (String)e.getValue());
} else {
builder.put(e.getKey(), e.getValue().toString());
}
}
return builder.build();
}
private Recordable convertToRecordable(long id, Document o) {
return new RecordableImpl(id, (String) o.get(RECORDABLE_TEXT),
(int) o.get(RECORDABLE_BEGIN), (int) o.get(RECORDABLE_END),
(String) o.get(RECORDABLE_TYPE));
}
}