package org.atlasapi.persistence.lookup.mongo;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.equalTo;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
import static com.metabroadcast.common.persistence.mongo.MongoBuilders.select;
import static com.metabroadcast.common.persistence.mongo.MongoBuilders.sort;
import static com.metabroadcast.common.persistence.mongo.MongoBuilders.where;
import static com.metabroadcast.common.persistence.mongo.MongoConstants.ID;
import static com.metabroadcast.common.persistence.mongo.MongoConstants.IN;
import static com.metabroadcast.common.persistence.mongo.MongoConstants.SINGLE;
import static com.metabroadcast.common.persistence.mongo.MongoConstants.UPSERT;
import static org.atlasapi.media.entity.LookupRef.TO_URI;
import static org.atlasapi.persistence.lookup.entry.LookupEntry.lookupEntryFrom;
import static org.atlasapi.persistence.lookup.mongo.LookupEntryTranslator.ACTIVELY_PUBLISHED;
import static org.atlasapi.persistence.lookup.mongo.LookupEntryTranslator.ALIASES;
import static org.atlasapi.persistence.lookup.mongo.LookupEntryTranslator.IDS;
import static org.atlasapi.persistence.lookup.mongo.LookupEntryTranslator.OPAQUE_ID;
import static org.atlasapi.persistence.lookup.mongo.LookupEntryTranslator.SELF;
import static org.atlasapi.persistence.media.entity.AliasTranslator.NAMESPACE;
import static org.atlasapi.persistence.media.entity.AliasTranslator.VALUE;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.atlasapi.media.entity.Content;
import org.atlasapi.media.entity.LookupRef;
import org.atlasapi.media.entity.Publisher;
import org.atlasapi.persistence.audit.PersistenceAuditLog;
import org.atlasapi.persistence.content.ContentCategory;
import org.atlasapi.persistence.content.listing.ContentListingProgress;
import org.atlasapi.persistence.lookup.NewLookupWriter;
import org.atlasapi.persistence.lookup.entry.LookupEntry;
import org.atlasapi.persistence.lookup.entry.LookupEntryStore;
import org.atlasapi.persistence.media.entity.IdentifiedTranslator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.metabroadcast.common.persistence.mongo.MongoBuilders;
import com.metabroadcast.common.persistence.mongo.MongoConstants;
import com.metabroadcast.common.persistence.mongo.MongoQueryBuilder;
import com.metabroadcast.common.persistence.translator.TranslatorUtils;
import com.metabroadcast.common.query.Selection;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.ReadPreference;
public class MongoLookupEntryStore implements LookupEntryStore, NewLookupWriter {
private static final String PUBLISHER = SELF + "." + IdentifiedTranslator.PUBLISHER;
private static final Pattern ANYTHING = Pattern.compile("^.*");
private static final Function<ContentCategory, String> CONTENT_CATEGORY_TO_NAME =
new Function<ContentCategory, String>() {
@Nullable @Override public String apply(@Nullable ContentCategory input) {
return input != null ? input.name() : null;
}
};
private final Logger log;
private final DBCollection lookup;
private final LookupEntryTranslator translator;
private final ReadPreference readPreference;
private final LookupEntryHasher lookupEntryHasher;
private final PersistenceAuditLog persistenceAuditLog;
public MongoLookupEntryStore(DBCollection lookup, PersistenceAuditLog persistenceAuditLog,
ReadPreference readPreference) {
this(lookup, readPreference, persistenceAuditLog, LoggerFactory.getLogger(MongoLookupEntryStore.class));
}
public MongoLookupEntryStore(DBCollection lookup, ReadPreference readPreference,
PersistenceAuditLog persistenceAuditLog, Logger log) {
this.lookup = checkNotNull(lookup);
this.readPreference = checkNotNull(readPreference);
this.persistenceAuditLog = checkNotNull(persistenceAuditLog);
this.translator = new LookupEntryTranslator();
this.lookupEntryHasher = new LookupEntryHasher(translator);
this.log = checkNotNull(log);
}
@Override
public void store(LookupEntry entry) {
LookupEntry existing = translator.fromDbo(lookup.findOne(new BasicDBObject(MongoConstants.ID, entry.uri()), null, ReadPreference.primary()));
store(entry, existing);
}
private void store(LookupEntry newEntry, @Nullable LookupEntry existingEntry) {
if (existingEntry != null
&& lookupEntryHasher.writeHashFor(newEntry) == lookupEntryHasher.writeHashFor(existingEntry)) {
log.debug("Hash code not changed for URI {}; skipping write", newEntry.uri());
persistenceAuditLog.logNoWrite(newEntry);
return;
}
log.debug("New entry or hash code changed for URI {}; writing", newEntry.uri());
persistenceAuditLog.logWrite(newEntry);
lookup.update(MongoBuilders.where().idEquals(newEntry.uri()).build(), translator.toDbo(newEntry), UPSERT, SINGLE);
}
@Override
public Iterable<LookupEntry> entriesForCanonicalUris(Iterable<String> uris) {
DBCursor found = lookup.find(where().idIn(uris).build()).setReadPreference(readPreference);
if (found == null) {
return ImmutableList.of();
}
return Iterables.transform(found, translator.FROM_DBO);
}
@Override
public Iterable<LookupEntry> entriesForIds(Iterable<Long> ids) {
DBObject queryDbo = new BasicDBObject(OPAQUE_ID, new BasicDBObject(IN, ids));
DBCursor found = lookup.find(queryDbo).setReadPreference(readPreference);
if (found == null) {
return ImmutableList.of();
}
return Iterables.transform(found, translator.FROM_DBO);
}
@Override
public void ensureLookup(Content content) {
LookupEntry newEntry = lookupEntryFrom(content);
// Since most content will already have a lookup entry we read first to avoid locking the database
LookupEntry existing = translator.fromDbo(lookup.findOne(new BasicDBObject(MongoConstants.ID, content.getCanonicalUri()), null, ReadPreference.primary()));
if (existing == null) {
store(newEntry, existing);
} else if(!newEntry.lookupRef().category().equals(existing.lookupRef().category())) {
updateEntry(content, newEntry, existing);
} else if (!newEntry.aliasUrls().equals(existing.aliasUrls())
|| !newEntry.aliases().equals(existing.aliases())
|| newEntry.activelyPublished() != existing.activelyPublished()) {
store(merge(content, newEntry, existing), existing);
}
}
private void updateEntry(Content content, LookupEntry newEntry, LookupEntry existing) {
LookupEntry merged = merge(content, newEntry, existing);
LookupRef ref = merged.lookupRef();
store(merged, existing);
for (LookupEntry entry : entriesForCanonicalUris(transform(filter(merged.equivalents(), not(equalTo(ref))), TO_URI))) {
if(entry.directEquivalents().contains(ref)) {
entry = entry.copyWithDirectEquivalents(ImmutableSet.<LookupRef>builder().add(ref).addAll(entry.directEquivalents()).build());
}
entry = entry.copyWithEquivalents(ImmutableSet.<LookupRef>builder().add(ref).addAll(existing.equivalents()).build());
store(entry, existing);
}
}
private LookupEntry merge(Content content, LookupEntry newEntry, LookupEntry existing) {
LookupRef ref = LookupRef.from(content);
Set<LookupRef> directEquivs = ImmutableSet.<LookupRef>builder().add(ref).addAll(existing.directEquivalents()).build();
Set<LookupRef> explicit = ImmutableSet.<LookupRef>builder().add(ref).addAll(existing.explicitEquivalents()).build();
Set<LookupRef> transitiveEquivs = ImmutableSet.<LookupRef>builder().add(ref).addAll(existing.equivalents()).build();
LookupEntry merged = new LookupEntry(newEntry.uri(), existing.id(), ref, newEntry.aliasUrls(), newEntry.aliases(), directEquivs, explicit, transitiveEquivs, existing.created(), newEntry.updated(), newEntry.activelyPublished());
return merged;
}
@Override
public Iterable<LookupEntry> entriesForIdentifiers(Iterable<String> identifiers, boolean useAliases) {
return Iterables.transform(find(identifiers), translator.FROM_DBO);
}
private Iterable<DBObject> find(Iterable<String> identifiers) {
return lookup.find(where().fieldIn(ALIASES, identifiers).build()).setReadPreference(readPreference);
}
@Override
public Iterable<LookupEntry> entriesForAliases(Optional<String> namespace, Iterable<String> values) {
return Iterables.transform(find(namespace, values), translator.FROM_DBO);
}
@Override
public Map<String, Long> idsForCanonicalUris(Iterable<String> uris) {
Builder<String, Long> results = ImmutableMap.builder();
DBCursor cursor = lookup.find(
where().idIn(uris).build(),
select().field(OPAQUE_ID).field(ID).build()
)
.setReadPreference(readPreference);
for (DBObject dbo : cursor) {
Long id = TranslatorUtils.toLong(dbo, OPAQUE_ID);
if (id != null) {
results.put(TranslatorUtils.toString(dbo, ID), id);
}
}
return results.build();
}
private Iterable<DBObject> find(Optional<String> namespace, Iterable<String> values) {
if (namespace.isPresent()) {
return lookup.find(where().elemMatch(IDS, where().fieldEquals(NAMESPACE, namespace.get()).fieldIn(VALUE, values)).build())
.setReadPreference(readPreference);
} else {
return lookup.find(where().elemMatch(IDS, where().fieldEquals(NAMESPACE, ANYTHING).fieldIn(VALUE, values)).build())
.setReadPreference(readPreference);
}
}
@Override
public Iterable<LookupEntry> entriesForPublishers(Iterable<Publisher> publishers,
@Nullable Selection selection) {
DBCursor find = lookup.find(where()
.fieldIn(PUBLISHER, Iterables.transform(publishers, Publisher.TO_KEY))
// Not actively published content will have this value set to false
// Actively published content will either have this value be true or null
.fieldNotEqualTo(ACTIVELY_PUBLISHED, false)
.build()
)
.setReadPreference(readPreference)
.sort(sort().ascending(OPAQUE_ID).build());
Iterable<DBObject> result;
if (selection != null) {
find.skip(selection.getOffset());
result = Iterables.limit(find, selection.getLimit());
} else {
result = find;
}
return Iterables.transform(result, translator.FROM_DBO);
}
@Override
public Iterable<LookupEntry> allEntriesForPublishers(Iterable<Publisher> publishers,
ContentListingProgress progress) {
DBCursor cursor = cursorForPublishers(publishers, progress);
return Iterables.transform(cursor, translator.FROM_DBO);
}
public Iterable<LookupEntry> all() {
return Iterables.transform(lookup.find(), translator.FROM_DBO);
}
private DBCursor cursorForPublishers(Iterable<Publisher> publishers,
ContentListingProgress progress) {
MongoQueryBuilder queryBuilder = where()
.fieldIn(PUBLISHER, Iterables.transform(publishers, Publisher.TO_KEY));
if (!progress.equals(ContentListingProgress.START)) {
limitQueryByProgress(progress, queryBuilder);
}
return lookup.find(queryBuilder.build())
.setReadPreference(readPreference)
.sort(sort().ascending(OPAQUE_ID).build());
}
private void limitQueryByProgress(ContentListingProgress progress,
MongoQueryBuilder queryBuilder) {
Iterable<LookupEntry> progressedToEntry = entriesForCanonicalUris(
ImmutableList.of(progress.getUri())
);
if (Iterables.isEmpty(progressedToEntry)) {
return;
}
LookupEntry entry = Iterables.getOnlyElement(progressedToEntry);
queryBuilder.fieldGreaterThan(OPAQUE_ID, entry.id());
}
}