package org.atlasapi.persistence.output; import static com.google.common.base.Preconditions.checkNotNull; import static com.metabroadcast.common.persistence.mongo.MongoBuilders.select; import static com.metabroadcast.common.persistence.mongo.MongoBuilders.where; import static com.metabroadcast.common.persistence.translator.TranslatorUtils.toDBObject; import static com.metabroadcast.common.persistence.translator.TranslatorUtils.toDBObjectList; import static com.metabroadcast.common.persistence.translator.TranslatorUtils.toDateTime; import java.util.Collection; import java.util.Comparator; import java.util.Map; import com.metabroadcast.applications.client.model.internal.Application; import org.atlasapi.media.entity.ChildRef; import org.atlasapi.media.entity.Container; import org.atlasapi.media.entity.EntityType; import org.atlasapi.media.entity.Item; import org.atlasapi.media.entity.LookupRef; import org.atlasapi.media.entity.Person; import org.atlasapi.media.entity.Publisher; import org.atlasapi.persistence.content.ContentCategory; import org.atlasapi.persistence.lookup.entry.LookupEntry; import org.atlasapi.persistence.lookup.entry.LookupEntryStore; import org.atlasapi.persistence.media.entity.DescribedTranslator; import org.atlasapi.persistence.media.entity.IdentifiedTranslator; import org.joda.time.DateTime; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Ordering; import com.metabroadcast.common.base.MorePredicates; 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.persistence.translator.TranslatorUtils; import com.metabroadcast.common.time.Clock; import com.metabroadcast.common.time.DateTimeZones; import com.metabroadcast.common.time.SystemClock; import com.mongodb.DBCollection; import com.mongodb.DBObject; /** * Resolves URIs of available items of a given Container. * * Queries Mongo for available items of a primary Container and its * equivalents (filtered by those available to application configuration). * * The URIs are converted back to URIs of the primary Container's source via * resolution of their LookupEntries. * */ public class MongoAvailableItemsResolver implements AvailableItemsResolver { private static final Ordering<DBObject> AVAILABILITY_START_ORDERING = Ordering.from(new Comparator<DBObject>() { @Override public int compare(DBObject o1, DBObject o2) { DateTime one = earliestAvailabilityStart(o1); DateTime two = earliestAvailabilityStart(o2); if (one == null && two == null) { return 0; } if (one == null) { return -1; } if (two == null) { return 1; } return one.compareTo(two); } private DateTime earliestAvailabilityStart(DBObject dbo) { DateTime earliest = new DateTime(DateTimeZones.UTC); for (DBObject version : toDBObjectList(dbo, versions)) { for (DBObject encoding : toDBObjectList(version, encodings)) { for (DBObject location : toDBObjectList(encoding, locations)) { DBObject policy = toDBObject(location, MongoAvailableItemsResolver.policy); if (policy != null) { DateTime start = toDateTime(policy, availabilityStart); if (availableBefore(start, earliest)) { earliest = start; } } } } } return earliest; } }); private static final String versions = "versions"; private static final String encodings = "manifestedAs"; private static final String locations = "availableAt"; private static final String policy = "policy"; private static final String availabilityStart = "availabilityStart"; private static final String availabilityEnd = "availabilityEnd"; private final String availabilityStartKey = String.format("%s.%s.%s.%s.%s", versions, encodings, locations, policy, availabilityStart); private final String availabilityEndKey = String.format("%s.%s.%s.%s.%s", versions, encodings, locations, policy, availabilityEnd); private final String containerKey = "container"; private final DBObject fields = select().fields(ImmutableSet.of(IdentifiedTranslator.TYPE, IdentifiedTranslator.OPAQUE_ID, IdentifiedTranslator.LAST_UPDATED, DescribedTranslator.PUBLISHER_KEY, availabilityStartKey, availabilityEndKey)).build(); private final LookupEntryStore entryStore; private final DBCollection children; private final DBCollection topLevelItems; private final Clock clock; public MongoAvailableItemsResolver(DatabasedMongo db, LookupEntryStore entryStore) { this(db, entryStore, new SystemClock()); } public MongoAvailableItemsResolver(DatabasedMongo db, LookupEntryStore entryStore, Clock clock) { this.children = db.collection(ContentCategory.CHILD_ITEM.tableName()); this.topLevelItems = db.collection(ContentCategory.TOP_LEVEL_ITEM.tableName()); this.entryStore = checkNotNull(entryStore); this.clock = clock; } @Override public Iterable<ChildRef> availableItemsFor(Container container, Application application) { return switchEquivs(availableItemsByPublisherFor(container, application).values(), container.getPublisher()); } @Override public Multimap<Publisher, ChildRef> availableItemsByPublisherFor(Item item, Application application) { final DateTime now = clock.now(); DBObject query = where() .idIn(FluentIterable.from(item.getEquivalentTo()) .filter(sourceFilter(application.getConfiguration().getEnabledReadSources())) .transform(LookupRef.TO_URI) ) .or(new MongoQueryBuilder().doesNotExist(availabilityEnd), new MongoQueryBuilder().fieldAfter(availabilityEnd, now)) //.fieldAfter(availabilityEndKey, now) .build(); return filterItems(now, Iterables.concat(children.find(query,fields), topLevelItems.find(query,fields))); } @Override public Multimap<Publisher, ChildRef> availableItemsByPublisherFor(Container container, Application application) { final DateTime now = clock.now(); return filterItems(now, sortByAvailabilityStart(availablityWindowsForItemsOf(container, application, now))); } @Override /* * TODO: make this work better for Person from schedule-only sources. * * At the moment we only look at the contents of the Person directly - for * more availability the LookupEntrys for the Person's contents should be * resolved and the entire set of all equivalents tested, switching back to * the Person's content identifiers for the final result. * * So if a Person has Item I, resolve LE for I, and I -> (J, K), test * (I,J,K), J is available, return I. */ public Iterable<ChildRef> availableItemsFor(Person person, Application application) { final DateTime now = clock.now(); return switchEquivs(filterItems(now, sortByAvailabilityStart(availablityWindowsForItemsOf(person, now))).values(), person.getPublisher()); } //switch URIs of available items to URIs of the containers source via their lookup refs. private Iterable<ChildRef> switchEquivs(Iterable<ChildRef> childRefs, final Publisher source) { Map<String, LookupEntry> entries = Maps.uniqueIndex(entryStore.entriesForCanonicalUris( Iterables.transform(childRefs, ChildRef.TO_URI)), LookupEntry.TO_ID); Builder<ChildRef> rewrittenRefs = ImmutableSet.builder(); Predicate<LookupRef> sourceFilter = sourceFilter(ImmutableSet.of(source)); for (ChildRef childRef : childRefs) { Iterable<LookupRef> sourceRefs = Iterables.filter(entries.get(childRef.getUri()).equivalents(), sourceFilter); if (Iterables.size(sourceRefs) > 0) { LookupRef lookupRef = sourceRefs.iterator().next(); rewrittenRefs.add(new ChildRef(lookupRef.id(), lookupRef.uri(), childRef.getSortKey(), childRef.getUpdated(), childRef.getType() )); } else { // There is no equivalent item in the target source; ignore this reference } } return rewrittenRefs.build(); } private Predicate<LookupRef> sourceFilter(Collection<Publisher> sources) { return MorePredicates.transformingPredicate(LookupRef.TO_SOURCE, Predicates.in(sources)); } private Iterable<DBObject> sortByAvailabilityStart(Iterable<DBObject> childDbos) { return AVAILABILITY_START_ORDERING.immutableSortedCopy(childDbos); } private Multimap<Publisher, ChildRef> filterItems(final DateTime now, Iterable<DBObject> availableItems) { ImmutableMultimap.Builder<Publisher, ChildRef> itemMap = ImmutableMultimap.builder(); for (DBObject availableItem : availableItems) { for (DBObject version : toDBObjectList(availableItem, versions)) { for (DBObject encoding : toDBObjectList(version, encodings)) { for (DBObject location : toDBObjectList(encoding, locations)) { DBObject policy = toDBObject(location, MongoAvailableItemsResolver.policy); if (policy != null && availableBefore(toDateTime(policy, availabilityStart), now) && availableAfter(toDateTime(policy, availabilityEnd), now)) { String uri = TranslatorUtils.toString(availableItem, MongoConstants.ID); Long aid = TranslatorUtils.toLong(availableItem, IdentifiedTranslator.OPAQUE_ID); String type = TranslatorUtils.toString(availableItem, IdentifiedTranslator.TYPE); DateTime lastUpdated = TranslatorUtils.toDateTime(availableItem, IdentifiedTranslator.LAST_UPDATED); Publisher publisher = Publisher.fromKey(TranslatorUtils.toString(availableItem, DescribedTranslator.PUBLISHER_KEY) ).requireValue(); itemMap.put(publisher, new ChildRef(aid, uri, "", lastUpdated, EntityType.from(type))); } } } } } return itemMap.build(); } private static boolean availableBefore(DateTime availabilityStart, DateTime now) { return availabilityStart == null || availabilityStart.isBefore(now); } private boolean availableAfter(DateTime availabilityEnd, DateTime now) { return availabilityEnd == null || availabilityEnd.isAfter(now); } // find available items for container and its equivalents from enabled sources private Iterable<DBObject> availablityWindowsForItemsOf(Container container, Application application, DateTime time) { DBObject query = where() .fieldIn(containerKey, containerAndEquivalents(container, application)) .or(new MongoQueryBuilder().doesNotExist(availabilityEnd), new MongoQueryBuilder().fieldAfter(availabilityEnd, time)) //.fieldAfter(availabilityEndKey, time) .build(); return children.find(query,fields); } private Iterable<String> containerAndEquivalents(Container container, final Application application) { return Iterables.concat(ImmutableSet.of(container.getCanonicalUri()), Iterables.transform(Iterables.filter( container.getEquivalentTo(), sourceFilter(application.getConfiguration().getEnabledReadSources())), LookupRef.TO_URI)); } // find available items for container and its equivalents from enabled sources private Iterable<DBObject> availablityWindowsForItemsOf(Person person, DateTime time) { DBObject query = where() .idIn(Iterables.transform(person.getContents(), ChildRef.TO_URI)) .fieldAfter(availabilityEndKey, time) .build(); return Iterables.concat(children.find(query,fields), topLevelItems.find(query,fields)); } }