package org.javers.repository.mongo; import com.google.gson.JsonObject; import com.mongodb.BasicDBObject; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Filters; import org.bson.Document; import org.bson.conversions.Bson; import java.util.Optional; import org.javers.common.string.RegexEscape; import org.javers.core.commit.Commit; import org.javers.core.commit.CommitId; import org.javers.core.json.JsonConverter; import org.javers.core.json.typeadapter.util.UtilTypeCoreAdapters; import org.javers.core.metamodel.object.CdoSnapshot; import org.javers.core.metamodel.object.GlobalId; import org.javers.core.metamodel.type.EntityType; import org.javers.core.metamodel.type.ManagedType; import org.javers.core.metamodel.type.ValueObjectType; import org.javers.repository.api.JaversRepository; import org.javers.repository.api.QueryParams; import org.javers.repository.api.QueryParamsBuilder; import org.javers.repository.api.SnapshotIdentifier; import org.javers.repository.mongo.model.MongoHeadId; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static org.javers.common.collections.Lists.toImmutableList; import static org.javers.common.validation.Validate.conditionFulfilled; import static org.javers.repository.mongo.DocumentConverter.fromDocument; import static org.javers.repository.mongo.DocumentConverter.toDocument; import static org.javers.repository.mongo.MongoSchemaManager.*; /** * @author pawel szymczyk */ public class MongoRepository implements JaversRepository { private final static int DEFAULT_CACHE_SIZE = 5000; private static final int DESC = -1; private final MongoSchemaManager mongoSchemaManager; private JsonConverter jsonConverter; private final MapKeyDotReplacer mapKeyDotReplacer = new MapKeyDotReplacer(); private final LatestSnapshotCache cache; public MongoRepository(MongoDatabase mongo) { this(mongo, null, DEFAULT_CACHE_SIZE); } /** * @param cacheSize Size of the latest snapshots cache, default is 5000. Set 0 to disable. */ public MongoRepository(MongoDatabase mongo, int cacheSize) { this(mongo, null, cacheSize); } MongoRepository(MongoDatabase mongo, JsonConverter jsonConverter, int cacheSize) { this.jsonConverter = jsonConverter; this.mongoSchemaManager = new MongoSchemaManager(mongo); cache = new LatestSnapshotCache(cacheSize, input -> getLatest(createIdQuery(input))); } @Override public void persist(Commit commit) { persistSnapshots(commit); persistHeadId(commit); } void clean(){ snapshotsCollection().deleteMany(new Document()); headCollection().deleteMany(new Document()); } @Override public List<CdoSnapshot> getStateHistory(GlobalId globalId, QueryParams queryParams) { Bson query; if (queryParams.isAggregate()){ query = createIdQueryWithAggregate(globalId); } else { query = createIdQuery(globalId); } return queryForSnapshots(query, Optional.of(queryParams)); } @Override public Optional<CdoSnapshot> getLatest(GlobalId globalId) { return cache.getLatest(globalId); } @Override public List<CdoSnapshot> getSnapshots(QueryParams queryParams) { return queryForSnapshots(new BasicDBObject(), Optional.of(queryParams)); } @Override public List<CdoSnapshot> getSnapshots(Collection<SnapshotIdentifier> snapshotIdentifiers) { return snapshotIdentifiers.isEmpty() ? Collections.<CdoSnapshot>emptyList() : queryForSnapshots(createSnapshotIdentifiersQuery(snapshotIdentifiers), Optional.<QueryParams>empty()); } @Override public List<CdoSnapshot> getValueObjectStateHistory(EntityType ownerEntity, String path, QueryParams queryParams) { BasicDBObject query = new BasicDBObject(GLOBAL_ID_OWNER_ID_ENTITY, ownerEntity.getName()); query.append(GLOBAL_ID_FRAGMENT, path); return queryForSnapshots(query, Optional.of(queryParams)); } @Override public List<CdoSnapshot> getStateHistory(Set<ManagedType> givenClasses, QueryParams queryParams) { Bson query = createManagedTypeQuery(givenClasses, queryParams.isAggregate()); return queryForSnapshots(query, Optional.of(queryParams)); } @Override public CommitId getHeadId() { Document headId = headCollection().find().first(); if (headId == null) { return null; } return new MongoHeadId(headId).toCommitId(); } @Override public void setJsonConverter(JsonConverter jsonConverter) { this.jsonConverter = jsonConverter; } @Override public void ensureSchema() { mongoSchemaManager.ensureSchema(); } private Bson createIdQuery(GlobalId id) { return new BasicDBObject(GLOBAL_ID_KEY, id.value()); } private Bson createIdQueryWithAggregate(GlobalId id) { return Filters.or(createIdQuery(id), prefixQuery(GLOBAL_ID_KEY, id.value() + "#")); } private Bson createVersionQuery(Long version) { return new BasicDBObject(SNAPSHOT_VERSION, version); } private Bson createSnapshotIdentifiersQuery(Collection<SnapshotIdentifier> snapshotIdentifiers) { List<Bson> descFilters = snapshotIdentifiers.stream().map( snapshotIdentifier -> Filters.and( createIdQuery(snapshotIdentifier.getGlobalId()), createVersionQuery(snapshotIdentifier.getVersion()) )).collect(toImmutableList()); return Filters.or(descFilters); } private Bson createManagedTypeQuery(Set<ManagedType> managedTypes, boolean aggregate) { List<Bson> classFilters = managedTypes.stream().map( managedType -> { if (managedType instanceof ValueObjectType) { return createValueObjectTypeQuery(managedType); } else { return createEntityTypeQuery(aggregate, managedType); } }).collect(toImmutableList()); return Filters.or(classFilters); } private Bson createValueObjectTypeQuery(ManagedType managedType) { return new BasicDBObject(GLOBAL_ID_VALUE_OBJECT, managedType.getName()); } private Bson createEntityTypeQuery(boolean aggregate, ManagedType managedType) { Bson entityTypeQuery = prefixQuery(GLOBAL_ID_KEY, managedType.getName() + "/"); if (!aggregate) { entityTypeQuery = Filters.and(entityTypeQuery, Filters.exists(GLOBAL_ID_ENTITY)); } return entityTypeQuery; } private CdoSnapshot readFromDBObject(Document dbObject) { return jsonConverter.fromJson(fromDocument(mapKeyDotReplacer.back(dbObject)), CdoSnapshot.class); } private Document writeToDBObject(CdoSnapshot snapshot){ conditionFulfilled(jsonConverter != null, "MongoRepository: jsonConverter is null"); Document dbObject = toDocument((JsonObject)jsonConverter.toJsonElement(snapshot)); dbObject = mapKeyDotReplacer.replaceInSnapshotState(dbObject); dbObject.append(GLOBAL_ID_KEY,snapshot.getGlobalId().value()); return dbObject; } private MongoCollection<Document> snapshotsCollection() { return mongoSchemaManager.snapshotsCollection(); } private MongoCollection<Document> headCollection() { return mongoSchemaManager.headCollection(); } private void persistSnapshots(Commit commit) { MongoCollection<Document> collection = snapshotsCollection(); commit.getSnapshots().forEach(snapshot -> { collection.insertOne(writeToDBObject(snapshot)); cache.put(snapshot); }); } private void persistHeadId(Commit commit) { MongoCollection<Document> headIdCollection = headCollection(); Document oldHead = headIdCollection.find().first(); MongoHeadId newHeadId = new MongoHeadId(commit.getId()); if (oldHead == null) { headIdCollection.insertOne(newHeadId.toDocument()); } else { headIdCollection.updateOne(objectIdFiler(oldHead), newHeadId.getUpdateCommand()); } } private Bson objectIdFiler(Document document) { return Filters.eq(OBJECT_ID, document.getObjectId("_id")); } private MongoCursor<Document> getMongoSnapshotsCursor(Bson query, Optional<QueryParams> queryParams) { FindIterable<Document> findIterable = snapshotsCollection() .find(applyQueryParams(query, queryParams)) .sort(new Document(COMMIT_ID, DESC)); return applyQueryParams(findIterable, queryParams).iterator(); } private Bson applyQueryParams(Bson query, Optional<QueryParams> queryParams) { if (queryParams.isPresent()) { QueryParams params = queryParams.get(); if (params.from().isPresent()) { query = addFromDateFiler(query, params.from().get()); } if (params.to().isPresent()) { query = addToDateFilter(query, params.to().get()); } if (params.commitIds().size() > 0) { query = addCommitIdFilter(query, params.commitIds()); } if (params.version().isPresent()) { query = addVersionFilter(query, params.version().get()); } if (params.author().isPresent()) { query = addAuthorFilter(query, params.author().get()); } if (!params.commitProperties().isEmpty()) { query = addCommitPropertiesFilter(query, params.commitProperties()); } if (params.changedProperty().isPresent()) { query = addChangedPropertyFilter(query, params.changedProperty().get()); } } return query; } private FindIterable<Document> applyQueryParams(FindIterable<Document> findIterable, Optional<QueryParams> queryParams) { if (queryParams.isPresent()) { QueryParams params = queryParams.get(); findIterable = findIterable .limit(params.limit()) .skip(params.skip()); } return findIterable; } private Bson addFromDateFiler(Bson query, LocalDateTime from) { return Filters.and(query, Filters.gte(COMMIT_DATE, UtilTypeCoreAdapters.serialize(from))); } private Bson addToDateFilter(Bson query, LocalDateTime to) { return Filters.and(query, Filters.lte(COMMIT_DATE, UtilTypeCoreAdapters.serialize(to))); } private Bson addCommitIdFilter(Bson query, Set<CommitId> commitIds) { return Filters.in(COMMIT_ID, commitIds.stream().map(c -> c.valueAsNumber().doubleValue()).collect(Collectors.toSet())); } private Bson addChangedPropertyFilter(Bson query, String changedProperty){ return Filters.and(query, new BasicDBObject(CHANGED_PROPERTIES, changedProperty)); } private Bson addVersionFilter(Bson query, Long version) { return Filters.and(query, createVersionQuery(version)); } private Bson addCommitPropertiesFilter(Bson query, Map<String, String> commitProperties) { List<Bson> propertyFilters = commitProperties.entrySet().stream().map( commitProperty -> new BasicDBObject(COMMIT_PROPERTIES, new BasicDBObject("$elemMatch", new BasicDBObject("key", commitProperty.getKey()).append( "value", commitProperty.getValue()))) ).collect(toImmutableList()); return Filters.and(query, Filters.and(propertyFilters.toArray(new Bson[]{}))); } private Bson addAuthorFilter(Bson query, String author) { return Filters.and(query, new BasicDBObject(COMMIT_AUTHOR, author)); } private Optional<CdoSnapshot> getLatest(Bson idQuery) { QueryParams queryParams = QueryParamsBuilder.withLimit(1).build(); MongoCursor<Document> mongoLatest = getMongoSnapshotsCursor(idQuery, Optional.of(queryParams)); return getOne(mongoLatest).map(d -> readFromDBObject(d)); } private List<CdoSnapshot> queryForSnapshots(Bson query, Optional<QueryParams> queryParams) { List<CdoSnapshot> snapshots = new ArrayList<>(); try (MongoCursor<Document> mongoSnapshots = getMongoSnapshotsCursor(query, queryParams)) { while (mongoSnapshots.hasNext()) { Document dbObject = mongoSnapshots.next(); snapshots.add(readFromDBObject(dbObject)); } return snapshots; } } private static <T> Optional<T> getOne(MongoCursor<T> mongoCursor){ try{ if (!mongoCursor.hasNext()) { return Optional.empty(); } return Optional.of(mongoCursor.next()); } finally { mongoCursor.close(); } } //enables index range scan private static Bson prefixQuery(String fieldName, String prefix){ return Filters.regex(fieldName, "^" + RegexEscape.escape(prefix) + ".*"); } }