package org.atlasapi.persistence.content.mongo; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.transform; 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.SINGLE; import static com.metabroadcast.common.persistence.mongo.MongoConstants.UPSERT; import java.util.Set; import org.atlasapi.media.entity.Brand; import org.atlasapi.media.entity.Broadcast; import org.atlasapi.media.entity.ChildRef; import org.atlasapi.media.entity.Clip; import org.atlasapi.media.entity.Container; import org.atlasapi.media.entity.Encoding; import org.atlasapi.media.entity.Episode; import org.atlasapi.media.entity.Item; import org.atlasapi.media.entity.Location; import org.atlasapi.media.entity.Policy; import org.atlasapi.media.entity.Series; import org.atlasapi.media.entity.SeriesRef; import org.atlasapi.media.entity.Version; import org.atlasapi.persistence.audit.PersistenceAuditLog; import org.atlasapi.persistence.content.ContentCategory; import org.atlasapi.persistence.content.ContentWriter; import org.atlasapi.persistence.lookup.NewLookupWriter; import org.atlasapi.persistence.media.entity.ContainerTranslator; import org.atlasapi.persistence.media.entity.DescribedTranslator; import org.atlasapi.persistence.media.entity.IdentifiedTranslator; import org.atlasapi.persistence.media.entity.ItemTranslator; import org.atlasapi.persistence.player.PlayerResolver; import org.atlasapi.persistence.service.ServiceResolver; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.metabroadcast.common.ids.NumberToShortStringCodec; import com.metabroadcast.common.ids.SubstitutionTableNumberCodec; import com.metabroadcast.common.persistence.mongo.DatabasedMongo; import com.metabroadcast.common.persistence.mongo.MongoConstants; import com.metabroadcast.common.persistence.mongo.MongoQueryBuilder; import com.metabroadcast.common.time.Clock; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBObject; public class MongoContentWriter implements ContentWriter { private static final Set<String> KEYS_TO_REMOVE = ImmutableSet.of(DescribedTranslator.LINKS_KEY); private final Logger log = LoggerFactory.getLogger(MongoContentWriter.class); private final Clock clock; private final NewLookupWriter lookupStore; private final PlayerResolver playerResolver; private final ServiceResolver serviceResolver; private final ItemTranslator itemTranslator; private final ContainerTranslator containerTranslator; private ChildRefWriter childRefWriter; private final DBCollection children; private final DBCollection topLevelItems; private final DBCollection containers; private final DBCollection programmeGroups; private final PersistenceAuditLog persistenceAuditLog; public MongoContentWriter(DatabasedMongo mongo, NewLookupWriter lookupStore, PersistenceAuditLog persistenceAuditLog, PlayerResolver playerResolver, ServiceResolver serviceResolver, Clock clock) { this.lookupStore = checkNotNull(lookupStore); this.clock = checkNotNull(clock); this.persistenceAuditLog = checkNotNull(persistenceAuditLog); this.playerResolver = checkNotNull(playerResolver); this.serviceResolver = checkNotNull(serviceResolver); MongoContentTables contentTables = new MongoContentTables(mongo); children = contentTables.collectionFor(ContentCategory.CHILD_ITEM); topLevelItems = contentTables.collectionFor(ContentCategory.TOP_LEVEL_ITEM); containers = contentTables.collectionFor(ContentCategory.CONTAINER); programmeGroups = contentTables.collectionFor(ContentCategory.PROGRAMME_GROUP); this.childRefWriter = new ChildRefWriter(mongo); NumberToShortStringCodec idCodec = new SubstitutionTableNumberCodec(); this.itemTranslator = new ItemTranslator(idCodec); this.containerTranslator = new ContainerTranslator(idCodec); } @Override public Item createOrUpdate(Item item) { checkNotNull(item, "Tried to persist null item"); setThisOrChildLastUpdated(item); item.setLastFetched(clock.now()); MongoQueryBuilder where = where().fieldEquals(IdentifiedTranslator.ID, item.getCanonicalUri()); if (!item.hashChanged(itemTranslator.hashCodeOf(item))) { log.debug("Item {} hash not changed. Not writing.", item.getCanonicalUri()); persistenceAuditLog.logNoWrite(item); return item; } validateRefs(item); persistenceAuditLog.logWrite(item); log.debug("Item {} hash changed so writing to db", item.getCanonicalUri()); if (item instanceof Episode) { if (item.getContainer() == null) { throw new IllegalArgumentException(String.format("Episodes must have containers: Episode %s", item.getCanonicalUri())); } childRefWriter.includeEpisodeInSeriesAndBrand((Episode) item); DBObject dbo = itemTranslator.toDB(item); children.update(where.build(), checkContainerRefs(dbo), UPSERT, SINGLE); remove(item.getCanonicalUri(), topLevelItems); } else if (item.getContainer() != null) { childRefWriter.includeItemInTopLevelContainer(item); DBObject dbo = itemTranslator.toDB(item); children.update(where.build(), checkContainerRefs(dbo), UPSERT, SINGLE); remove(item.getCanonicalUri(), topLevelItems); } else { topLevelItems.update(where.build(), itemTranslator.toDB(item), UPSERT, SINGLE); //disabled for now. need to remove the childref from the brand/series if enabled //remove(item.getCanonicalUri(), children); } lookupStore.ensureLookup(item); return item; } private void validateRefs(Item item) { for (Location location : allLocations(item)) { Policy policy = location.getPolicy(); if (policy != null) { if (policy.getService() != null) { checkState(serviceResolver.serviceFor(policy.getService()).isPresent(), "Service ID " + policy.getService() + " invalid"); } if (policy.getPlayer() != null) { checkState(playerResolver.playerFor(policy.getPlayer()).isPresent(), "Player ID " + policy.getPlayer() + " invalid"); } } ; } } private Iterable<Location> allLocations(Item item) { return concat(transform(allEncodings(item), Encoding.TO_LOCATIONS)); } private Iterable<Encoding> allEncodings(Item item) { return concat(transform(item.getVersions(), Version.TO_ENCODINGS)); } private DBObject checkContainerRefs(DBObject dbo) { checkContainerIdRef(dbo, ItemTranslator.CONTAINER, ItemTranslator.CONTAINER_ID); checkContainerIdRef(dbo, ItemTranslator.SERIES, ItemTranslator.SERIES_ID); return dbo; } private void checkContainerIdRef(DBObject dbo, String uriField, String idField) { if (dbo.containsField(uriField) && dbo.get(uriField) != null && (!dbo.containsField(idField) || dbo.get(idField) == null)) { log.warn("{} has {} {}, no id", new Object[]{ dbo.get(IdentifiedTranslator.ID), uriField, dbo.get(uriField)}); } } private void remove(String canonicalUri, DBCollection containingCollection) { DBObject find = containingCollection.findOne(new BasicDBObject(MongoConstants.ID, canonicalUri), new BasicDBObject(ID, 1)); if(find != null) { containingCollection.remove(new BasicDBObject(MongoConstants.ID, canonicalUri)); } } @Override public void createOrUpdate(Container container) { checkNotNull(container); checkArgument(container instanceof Brand || container instanceof Series, "Not brand or series"); setThisOrChildLastUpdated(container); container.setLastFetched(clock.now()); if (!container.hashChanged(containerTranslator.hashCodeOf(container))) { log.debug("Container {} hash not changed. Not writing.", container.getCanonicalUri()); persistenceAuditLog.logNoWrite(container); return; } persistenceAuditLog.logWrite(container); log.debug("Container {} hash changed so writing to db. There are {} ChildRefs", container.getCanonicalUri(), container.getChildRefs().size()); if (container instanceof Brand || isTopLevelSeries(container)) { DBObject containerDbo = containerTranslator.toDB(container); createOrUpdateContainer(container, containers, containerDbo); // The series inside a brand cannot be top level items any more so we // remove them as outer elements if (container instanceof Brand) { Brand brand = (Brand) container; Set<String> urisToRemove = Sets.newHashSet(Collections2.transform(brand.getSeriesRefs(), SeriesRef.TO_URI)); if (!urisToRemove.isEmpty()) { containers.remove(where().idIn(urisToRemove).build()); } } else { createOrUpdateContainer(container, programmeGroups, containerDbo); } } else { Series series = (Series)container; childRefWriter.includeSeriesInTopLevelContainer(series); DBObject dbo = containerTranslator.toDB(container); checkContainerIdRef(dbo, ContainerTranslator.CONTAINER, ContainerTranslator.CONTAINER_ID); createOrUpdateContainer(container, programmeGroups, dbo); //this isn't a top-level series so ensure it's not in the container table. containers.remove(where().idEquals(series.getCanonicalUri()).build()); } } private boolean isTopLevelSeries(Container container) { return container instanceof Series && ((Series)container).getParent() == null; } private void createOrUpdateContainer(Container container, DBCollection collection, DBObject containerDbo) { MongoQueryBuilder where = where().fieldEquals(IdentifiedTranslator.ID, container.getCanonicalUri()); BasicDBObject op = set(containerDbo); unset(containerDbo, op); collection.update(where.build(), op, true, false); lookupStore.ensureLookup(container); } /** * Since we do not perform a save() on containers, we must unset * keys that are not present in the object to be persisted. Since * there is no prototype object from which to unset fields, we * maintain a list of keys to perform unsets on. Limited to * related links at the moment, but can be extended. */ private void unset(DBObject dbo, BasicDBObject op) { BasicDBObject toRemove = new BasicDBObject(); for (String key : KEYS_TO_REMOVE) { if (!dbo.containsField(key)) { toRemove.put(key, 1); } } if (!toRemove.isEmpty()) { op.append(MongoConstants.UNSET, toRemove); } } private BasicDBObject set(DBObject dbo) { dbo.removeField(MongoConstants.ID); BasicDBObject containerUpdate = new BasicDBObject(MongoConstants.SET, dbo); return containerUpdate; } private void setThisOrChildLastUpdated(Item item) { DateTime thisOrChildLastUpdated = thisOrChildLastUpdated(null, item.getLastUpdated()); for (Clip clip : item.getClips()) { thisOrChildLastUpdated = thisOrChildLastUpdated(thisOrChildLastUpdated, clip.getLastUpdated()); } for (Version version : item.getVersions()) { thisOrChildLastUpdated = thisOrChildLastUpdated(thisOrChildLastUpdated, version.getLastUpdated()); for (Broadcast broadcast : version.getBroadcasts()) { thisOrChildLastUpdated = thisOrChildLastUpdated(thisOrChildLastUpdated, broadcast.getLastUpdated()); } for (Encoding encoding : version.getManifestedAs()) { thisOrChildLastUpdated = thisOrChildLastUpdated(thisOrChildLastUpdated, encoding.getLastUpdated()); for (Location location : encoding.getAvailableAt()) { thisOrChildLastUpdated = thisOrChildLastUpdated(thisOrChildLastUpdated, location.getLastUpdated()); } } } item.setThisOrChildLastUpdated(thisOrChildLastUpdated); } private DateTime setThisOrChildLastUpdated(Container playlist) { DateTime thisOrChildLastUpdated = thisOrChildLastUpdated(null, playlist.getLastUpdated()); for (ChildRef item : playlist.getChildRefs()) { DateTime itemOrChildUpdated = item.getUpdated(); thisOrChildLastUpdated = thisOrChildLastUpdated(thisOrChildLastUpdated, itemOrChildUpdated); } playlist.setThisOrChildLastUpdated(thisOrChildLastUpdated); return thisOrChildLastUpdated; } private DateTime thisOrChildLastUpdated(DateTime current, DateTime candidate) { if (candidate != null && (current == null || candidate.isAfter(current))) { return candidate; } return current; } }