package org.atlasapi.persistence.content.schedule.mongo;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import com.metabroadcast.applications.client.model.internal.Application;
import org.atlasapi.application.v3.DefaultApplication;
import org.atlasapi.equiv.OutputContentMerger;
import org.atlasapi.media.channel.Channel;
import org.atlasapi.media.channel.ChannelResolver;
import org.atlasapi.media.entity.Broadcast;
import org.atlasapi.media.entity.Content;
import org.atlasapi.media.entity.Encoding;
import org.atlasapi.media.entity.Identified;
import org.atlasapi.media.entity.Item;
import org.atlasapi.media.entity.Location;
import org.atlasapi.media.entity.Publisher;
import org.atlasapi.media.entity.Schedule;
import org.atlasapi.media.entity.ScheduleEntry;
import org.atlasapi.media.entity.ScheduleEntry.ItemRefAndBroadcast;
import org.atlasapi.media.entity.Version;
import org.atlasapi.messaging.v3.ScheduleUpdateMessage;
import org.atlasapi.output.Annotation;
import org.atlasapi.persistence.content.ContentResolver;
import org.atlasapi.persistence.content.EquivalentContent;
import org.atlasapi.persistence.content.EquivalentContentResolver;
import org.atlasapi.persistence.content.ResolvedContent;
import org.atlasapi.persistence.content.ScheduleResolver;
import org.atlasapi.persistence.content.schedule.ScheduleBroadcastFilter;
import org.atlasapi.persistence.media.entity.ScheduleEntryTranslator;
import com.metabroadcast.common.base.Maybe;
import com.metabroadcast.common.base.MorePredicates;
import com.metabroadcast.common.ids.NumberToShortStringCodec;
import com.metabroadcast.common.ids.SubstitutionTableNumberCodec;
import com.metabroadcast.common.persistence.mongo.DatabasedMongo;
import com.metabroadcast.common.queue.MessageSender;
import com.metabroadcast.common.queue.MessagingException;
import com.metabroadcast.common.time.SystemClock;
import com.metabroadcast.common.time.Timestamp;
import com.metabroadcast.common.time.Timestamper;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
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.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.mongodb.DBCollection;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.metabroadcast.common.persistence.mongo.MongoBuilders.where;
public class MongoScheduleStore implements ScheduleResolver, ScheduleWriter {
private final static Duration MAX_DURATION = Duration.standardDays(14);
private final ScheduleEntryBuilder scheduleEntryBuilder;
private final DBCollection collection;
private final ScheduleEntryTranslator translator;
private final EquivalentContentResolver equivalentContentResolver;
private final ChannelResolver channelResolver;
private final MessageSender<ScheduleUpdateMessage> messageSender;
private final Timestamper timestamper = new SystemClock();
private final NumberToShortStringCodec idCodec = SubstitutionTableNumberCodec.lowerCaseOnly();
private final Logger log = LoggerFactory.getLogger(getClass());
public MongoScheduleStore(
DatabasedMongo db,
ChannelResolver channelResolver,
ContentResolver contentResolver,
EquivalentContentResolver equivalentContentResolver,
MessageSender<ScheduleUpdateMessage> messageSender
) {
this.channelResolver = channelResolver;
this.contentResolver = contentResolver;
this.equivalentContentResolver = equivalentContentResolver;
collection = db.collection("schedule");
this.scheduleEntryBuilder = new ScheduleEntryBuilder(
channelResolver,
Duration.standardSeconds(Long.MAX_VALUE/1000)
);
translator = new ScheduleEntryTranslator(channelResolver);
this.messageSender = messageSender;
}
@Override
public void writeCompleteEntry(ScheduleEntry entry) {
collection.save(translator.toDb(entry));
}
@Override
public void writeScheduleFor(Iterable<? extends Item> items) {
Map<String, ScheduleEntry> scheduleEntries = scheduleEntryBuilder.toScheduleEntries(items);
Map<String, ScheduleEntry> existingEntries = Maps.uniqueIndex(translator.fromDbObjects(where().idIn(scheduleEntries.keySet()).find(collection)), ScheduleEntry.KEY);
for (ScheduleEntry entry: scheduleEntries.values()) {
ScheduleEntry updateEntry;
ScheduleEntry existingEntry = existingEntries.get(entry.toKey());
if (existingEntry == null) {
updateEntry = entry;
} else {
updateEntry = existingEntry;
updateEntry.withItems(Iterables.concat(updateEntry.getItemRefsAndBroadcasts(), entry.getItemRefsAndBroadcasts()));
}
writeCompleteEntry(updateEntry);
}
}
@Override
public void replaceScheduleBlock(Publisher publisher, Channel channel, Iterable<ItemRefAndBroadcast> itemsAndBroadcasts) {
Interval interval = checkAndGetScheduleInterval(itemsAndBroadcasts, true, channel);
Map<String, ScheduleEntry> entries = getAdjacentScheduleEntries(channel, publisher, interval);
for(ItemRefAndBroadcast itemAndBroadcast : itemsAndBroadcasts) {
scheduleEntryBuilder.toScheduleEntryFromBroadcast(channel, publisher, itemAndBroadcast, entries);
}
for (ScheduleEntry entry : entries.values()) {
writeCompleteEntry(entry);
}
sendUpdateMessage(publisher, channel, interval);
}
private void sendUpdateMessage(Publisher publisher, Channel channel, Interval interval) {
String mid = UUID.randomUUID().toString();
Timestamp ts = timestamper.timestamp();
String src = publisher.key();
String cid = idCodec.encode(BigInteger.valueOf(channel.getId()));
DateTime start = interval.getStart();
DateTime end = interval.getEnd();
ScheduleUpdateMessage msg = new ScheduleUpdateMessage(mid, ts, src, cid , start, end);
try {
messageSender.sendMessage(msg, msg.getChannel().getBytes());
} catch (MessagingException e) {
String errMsg = String.format("%s %s %s", src, cid, interval);
log.error(errMsg, e);
}
}
private Interval checkAndGetScheduleInterval(Iterable<ItemRefAndBroadcast> itemsAndBroadcasts, boolean allowGaps, Channel expectedChannel)
{
List<Broadcast> broadcasts = Lists.newArrayList();
for(ItemRefAndBroadcast itemAndBroadcast : itemsAndBroadcasts) {
broadcasts.add(itemAndBroadcast.getBroadcast());
}
Collections.<Broadcast>sort(broadcasts, new Comparator<Broadcast>() {
@Override
public int compare(Broadcast o1, Broadcast o2) {
if(o1.getTransmissionTime().equals(o2.getTransmissionTime())) {
return o1.getTransmissionEndTime().isBefore(o2.getTransmissionEndTime()) ? -1 : 1;
}
else {
return o1.getTransmissionTime().isBefore(o2.getTransmissionTime()) ? -1 : 1;
}
}
});
DateTime currentEndTime = null;
Broadcast previousBroadcast = null;
for(Broadcast b : broadcasts) {
if(!expectedChannel.equals(channelResolver.fromUri(b.getBroadcastOn()).requireValue())) {
throw new IllegalArgumentException("All broadcasts must be on the same channel; "
+ scheduleDebugInfo(previousBroadcast, b));
}
if(allowGaps) {
if(currentEndTime != null && b.getTransmissionTime().isBefore(currentEndTime)) {
throw new IllegalArgumentException("Overlapping periods found in schedule "
+ scheduleDebugInfo(previousBroadcast, b));
}
}
else {
if(currentEndTime != null && !(currentEndTime.equals(b.getTransmissionTime()))) {
throw new IllegalArgumentException("Schedule is not contiguous; "
+ scheduleDebugInfo(previousBroadcast, b));
}
}
currentEndTime = b.getTransmissionEndTime();
previousBroadcast = b;
}
return new Interval(broadcasts.get(0).getTransmissionTime(), currentEndTime);
}
private String scheduleDebugInfo(Broadcast previous, Broadcast current) {
if (previous == null) {
return String.format("On first broadcast, ID %s, starts %s", current.getSourceId(), current.getTransmissionTime());
}
return String.format("Last broadcast ID %s, ends %s; next broadcast ID %s, starts %s",
previous.getSourceId(), previous.getTransmissionEndTime().toString(),
current.getSourceId(), current.getTransmissionTime().toString());
}
private Map<String, ScheduleEntry> getAdjacentScheduleEntries(Channel channel,
Publisher publisher,
final Interval interval) {
List<Interval> intervals = scheduleEntryBuilder.intervalsFor(interval.getStart(), interval.getEnd());
Set<String> requiredScheduleEntries = Sets.<String>newHashSet();
String firstScheduleEntryKey = ScheduleEntry.toKey(intervals.get(0), channel, publisher);
String lastScheduleEntryKey = ScheduleEntry.toKey(intervals.get(intervals.size() - 1), channel, publisher);
requiredScheduleEntries.add(firstScheduleEntryKey);
requiredScheduleEntries.add(lastScheduleEntryKey);
Map<String, ScheduleEntry> scheduleEntries = Maps.newHashMap(Maps.uniqueIndex(translator.fromDbObjects(where().idIn(requiredScheduleEntries).find(collection)), ScheduleEntry.KEY));
Predicate<ItemRefAndBroadcast> beforePredicate = i -> i.getBroadcast()
.getTransmissionEndTime()
.isBefore(interval.getStart());
Predicate<ItemRefAndBroadcast> onBeforePredicate = i -> i.getBroadcast()
.getTransmissionEndTime()
.isEqual(interval.getStart());
Predicate<ItemRefAndBroadcast> onAfterPredicate = i -> i.getBroadcast()
.getTransmissionTime()
.isEqual(interval.getEnd());
Predicate<ItemRefAndBroadcast> afterPredicate = i -> i.getBroadcast()
.getTransmissionTime()
.isAfter(interval.getEnd());
// If the first and last period are identical,
// retain items that start before the interval or start after the end of the interval
if(firstScheduleEntryKey.equals(lastScheduleEntryKey)
&& scheduleEntries.containsKey(firstScheduleEntryKey)){
ScheduleEntry firstEntry = filteredScheduleEntry(
scheduleEntries.get(firstScheduleEntryKey),
Predicates.or(onBeforePredicate, beforePredicate, onAfterPredicate, afterPredicate)
);
scheduleEntries.put(firstScheduleEntryKey, firstEntry);
return scheduleEntries;
}
// For the first period, retain items that start before the interval
if(scheduleEntries.containsKey(firstScheduleEntryKey)) {
ScheduleEntry firstEntry = filteredScheduleEntry(
scheduleEntries.get(firstScheduleEntryKey),
Predicates.or(onBeforePredicate, beforePredicate)
);
scheduleEntries.put(firstScheduleEntryKey, firstEntry);
}
// For the last period, retain items that start on or after the end of the interval
if(scheduleEntries.containsKey(lastScheduleEntryKey)) {
ScheduleEntry lastEntry = filteredScheduleEntry(
scheduleEntries.get(lastScheduleEntryKey),
Predicates.or(onAfterPredicate, afterPredicate)
);
scheduleEntries.put(lastScheduleEntryKey, lastEntry);
}
return scheduleEntries;
}
private ScheduleEntry filteredScheduleEntry(ScheduleEntry entry, Predicate<ItemRefAndBroadcast> filterPredicate) {
Iterable<ItemRefAndBroadcast> filteredBroadcasts = Collections2.filter(entry.getItemRefsAndBroadcasts(), filterPredicate);
ScheduleEntry filteredEntry = new ScheduleEntry(entry.interval(), entry.channel(), entry.publisher(), filteredBroadcasts);
return filteredEntry;
}
@Override
public Schedule unmergedSchedule(DateTime from, DateTime to, Iterable<Channel> channels,
Iterable<Publisher> publishers) {
Interval interval = new Interval(from, to);
if (interval.toDuration().isLongerThan(MAX_DURATION)) {
throw new IllegalArgumentException("You cannot request more than 2 weeks of schedule");
}
List<ScheduleEntry> entries = resolveEntries(channels, from, to, publishers);
Iterable<Entry<Channel, ItemRefAndBroadcast>> uniqueRefs = uniqueRefs(entries);
Map<String, Maybe<Identified>> itemIndex = resolveItems(uniqueRefs);
return scheduleFrom(toChannelMap(channels, uniqueRefs, itemIndex), interval);
}
private Map<String, Maybe<Identified>> resolveItems(
Iterable<Entry<Channel, ItemRefAndBroadcast>> refs) {
ImmutableSet<String> uris = uniqueUris(refs);
ResolvedContent resolved = contentResolver.findByCanonicalUris(uris);
Builder<String, Maybe<Identified>> itemIndex = ImmutableMap.builder();
for (String uri : uris) {
itemIndex.put(uri, resolved.get(uri));
}
return itemIndex.build();
}
@Override
public Schedule schedule(
DateTime from,
DateTime to,
Iterable<Channel> channels,
Iterable<Publisher> publishers,
Optional<Application> applicationOptional
) {
Interval interval = new Interval(from, to);
if (interval.toDuration().isLongerThan(MAX_DURATION)) {
throw new IllegalArgumentException("You cannot request more than 2 weeks of schedule");
}
List<ScheduleEntry> entries = resolveEntries(channels, from, to, publishers);
Application application;
if(applicationOptional.isPresent()) {
application = applicationOptional.get();
} else {
application = configFor(publishers);
}
Map<Channel, List<Item>> channelMap = resolveEntries(entries, channels, application);
return scheduleFrom(channelMap, interval);
}
private Application configFor(Iterable<Publisher> publishers) {
return DefaultApplication.createWithReads(Lists.newArrayList(publishers));
}
private Map<Channel, List<Item>> resolveEntries(
Iterable<ScheduleEntry> entries,
Iterable<Channel> channels,
Application application
) {
// TODO this code inefficient, but in future we should avoid hydrating then items
// unless explicitly requested
Iterable<Entry<Channel, ItemRefAndBroadcast>> uniqueRefs = uniqueRefs(entries);
Map<String, Maybe<Identified>> itemIndex = resolveItems(uniqueRefs, application);
return toChannelMap(channels, uniqueRefs, itemIndex);
}
private Map<Channel, List<Item>> toChannelMap(Iterable<Channel> channels,
Iterable<Entry<Channel, ItemRefAndBroadcast>> uniqueRefs,
Map<String, Maybe<Identified>> itemIndex) {
SetMultimap<String, Broadcast> broadcastIndex = indexBroadcasts(itemIndex);
itemIndex = removeBroadcasts(itemIndex);
Map<Channel, List<Item>> channelMap = createChannelMap(channels);
for (Entry<Channel,ItemRefAndBroadcast> entry : uniqueRefs) {
String itemUri = entry.getValue().getItemUri();
final Maybe<Identified> possibleItem = itemIndex.get(itemUri);
if (possibleItem.hasValue()) {
Item item = ((Item) possibleItem.requireValue()).copy();
if (selectAndTrimBroadcast(broadcastIndex.get(itemUri), item, entry.getValue().getBroadcast())) {
channelMap.get(entry.getKey()).add(item);
}
}
}
return channelMap;
}
private SetMultimap<String, Broadcast> indexBroadcasts(Map<String, Maybe<Identified>> itemIndex) {
ImmutableSetMultimap.Builder<String, Broadcast> broadcasts = ImmutableSetMultimap.builder();
for (Entry<String, Maybe<Identified>> itemEntry : itemIndex.entrySet()) {
if (itemEntry.getValue().hasValue() && itemEntry.getValue().requireValue() instanceof Item) {
for (Version version : ((Item)itemEntry.getValue().requireValue()).getVersions()) {
broadcasts.putAll(itemEntry.getKey(), version.getBroadcasts());
}
}
}
return broadcasts.build();
}
private Map<String, Maybe<Identified>> removeBroadcasts(Map<String, Maybe<Identified>> itemIndex) {
for (Maybe<Identified> possibleItem : itemIndex.values()) {
if (possibleItem.hasValue() && possibleItem.requireValue() instanceof Item) {
removeBroadcasts((Item)possibleItem.requireValue());
}
}
return itemIndex;
}
private void removeBroadcasts(Item item) {
for (Version version : item.getVersions()) {
log.trace("Removing {} broadcasts from item {}", version.getBroadcasts().size(), item.getCanonicalUri());
version.setBroadcasts(ImmutableSet.<Broadcast>of());
}
}
private Map<String, Maybe<Identified>> resolveItems(
Iterable<Entry<Channel, ItemRefAndBroadcast>> refs,
Application mergeApplication
) {
return findAndMerge(mergeApplication, refs);
}
private ImmutableSet<String> uniqueUris(Iterable<Entry<Channel, ItemRefAndBroadcast>> refs) {
return ImmutableSet.copyOf(Iterables.transform(
refs,
input -> input.getValue().getItemUri()
));
}
private Map<String, Maybe<Identified>> findAndMerge(Application mergeApplication,
Iterable<Entry<Channel, ItemRefAndBroadcast>> refs) {
ImmutableSet<String> uris = uniqueUris(refs);
EquivalentContent resolved = equivalentContentResolver.resolveUris(
uris,
mergeApplication,
Annotation.defaultAnnotations(),
false
);
ImmutableMap.Builder<String, Maybe<Identified>> result = ImmutableMap.builder();
for (String uri : uris) {
Set<Content> equivalents = resolved.get(uri);
if (equivalents == null || equivalents.isEmpty()) {
result.put(uri, Maybe.<Identified>nothing());
} else {
//ensure the relevant schedule item is at the head of the Content
// list so it's selected as The Chosen One when merging equivalents
// and will bring balance to The Force.
ImmutableList<Content> equivList = ImmutableSet.<Content>builder()
.add(find(uri, equivalents))
.addAll(equivalents).build()
.asList();
result.put(uri, Maybe.<Identified>just(merger.merge(mergeApplication, equivList).get(0)));
}
}
return result.build();
}
private Iterable<Entry<Channel,ItemRefAndBroadcast>> uniqueRefs(Iterable<ScheduleEntry> entries) {
ImmutableSet.Builder<Entry<Channel, ItemRefAndBroadcast>> refs = ImmutableSet.builder();
for (ScheduleEntry scheduleEntry : entries) {
for (ItemRefAndBroadcast entryRef : scheduleEntry.getItemRefsAndBroadcasts()) {
refs.add(Maps.immutableEntry(scheduleEntry.channel(), entryRef));
}
}
return refs.build();
}
/*
* To resolve a schedule by count we need to guess how many hour buckets we
* need to fulfill the count. We start off by resolving a set window and
* counting how many unique item/broadcasts we got. While we don't have enough
* we repeatedly resolve the next window.
*/
@Override
public Schedule schedule(
final DateTime from,
int count,
Iterable<Channel> channels,
Iterable<Publisher> publishers,
Optional<Application> applicationOptional
) {
DateTime start = from;
DateTime end = from;
final Duration windowDuration = Duration.standardDays(1);
Map<Channel, ScheduleEntry> entries = Maps.newHashMap();
boolean reachedCount = false;
final int maxIterations = 2*count;
int iterations = 0;
Set<Channel> channelsNeedingMore = Sets.newHashSet(channels);
while(!reachedCount && iterations < maxIterations) {
start = end;
end = start.plus(windowDuration);
List<ScheduleEntry> windowEntries = resolveEntries(channelsNeedingMore, start, end, publishers);
entries = merge(from, count, entries, windowEntries);
for (ScheduleEntry scheduleEntry : entries.values()) {
if (scheduleEntry.getItemRefsAndBroadcasts().size() >= count) {
channelsNeedingMore.remove(scheduleEntry.channel());
}
}
reachedCount = channelsNeedingMore.isEmpty();
iterations++;
}
Application application;
if (applicationOptional.isPresent()) {
application = applicationOptional.get();
} else {
application = configFor(publishers);
}
Map<Channel, List<Item>> channelMap = resolveEntries(entries.values(), channels, application);
return scheduleFrom(channelMap, new Interval(from, end));
}
private Schedule scheduleFrom(Map<Channel, List<Item>> channelMap, Interval interval) {
ImmutableMap.Builder<Channel, List<Item>> processedChannelMap = ImmutableMap.builder();
for (Entry<Channel, List<Item>> entry: channelMap.entrySet()) {
processedChannelMap.put(entry.getKey(), processChannelItems(entry.getValue(), interval));
}
return Schedule.fromChannelMap(processedChannelMap.build(), interval);
}
private List<ScheduleEntry> resolveEntries(Iterable<Channel> channels, DateTime start, DateTime end,
Iterable<Publisher> publishers) {
List<String> keys = keys(scheduleEntryBuilder.intervalsFor(start, end), channels, publishers);
return translator.fromDbObjects(where().idIn(keys).find(collection));
}
private Map<Channel, ScheduleEntry> merge(
DateTime from,
int count,
Map<Channel, ScheduleEntry> existing,
List<ScheduleEntry> fetched
) {
for (ScheduleEntry scheduleEntry : fetched) {
ScheduleEntry existingEntry = existing.get(scheduleEntry.channel());
if (existingEntry != null) {
scheduleEntry = mergeScheduleEntries(existingEntry, scheduleEntry);
} else {
scheduleEntry = after(from, scheduleEntry);
}
existing.put(scheduleEntry.channel(), limitEntryItems(count, scheduleEntry));
}
return existing;
}
private ScheduleEntry after(final DateTime from, ScheduleEntry entry) {
return entry.withItems(Iterables.filter(entry.getItemRefsAndBroadcasts(),
input -> input.getBroadcast().getTransmissionEndTime().isAfter(from)
));
}
private ScheduleEntry limitEntryItems(int count, ScheduleEntry scheduleEntry) {
return scheduleEntry.withItems(Iterables.limit(scheduleEntry.getItemRefsAndBroadcasts(), count));
}
private ScheduleEntry mergeScheduleEntries(ScheduleEntry existing, ScheduleEntry fetched) {
return existing.withItems(Iterables.concat(existing.getItemRefsAndBroadcasts(), fetched.getItemRefsAndBroadcasts()));
}
private Content find(String uri, Set<Content> equivalents) {
for (Content content : equivalents) {
if (uri.equals(content.getCanonicalUri())) {
return content;
}
}
throw new IllegalStateException(uri + " not found in its own equivalent content set");
}
private boolean selectAndTrimBroadcast(Set<Broadcast> broadcasts, Item item, Broadcast scheduleBroadcast) {
boolean found = false;
for (Broadcast broadcast : broadcasts) {
if (scheduleBroadcast.equals(broadcast) && broadcast.isActivelyPublished()) {
addBroadcast(item, broadcast);
found = true;
}
}
return found;
}
private void addBroadcast(Item item, Broadcast broadcast) {
item.getVersions().iterator().next().setBroadcasts(ImmutableSet.of(broadcast));
}
private Map<Channel, List<Item>> createChannelMap(Iterable<Channel> channels) {
ImmutableMap.Builder<Channel, List<Item>> channelMap = ImmutableMap.builder();
for (Channel channel : channels) {
channelMap.put(channel, Lists.<Item> newArrayList());
}
return channelMap.build();
}
private List<Item> processChannelItems(Iterable<Item> items, final Interval interval) {
return filterLocations(orderItems(filterItems(items, interval)));
}
private List<Item> orderItems(Iterable<Item> items) {
return Ordering.from(ScheduleEntry.START_TIME_AND_DURATION_ITEM_COMPARATOR)
.immutableSortedCopy(items);
}
private static final Function<Broadcast, Interval> TO_BROADCAST = input ->
new Interval(input.getTransmissionTime(), input.getTransmissionEndTime());
private static final Function<Item, Interval> ITEM_TO_BROADCAST_INTERVAL = Functions.compose(
TO_BROADCAST,
ScheduleEntry.BROADCAST
);
private final OutputContentMerger merger = new OutputContentMerger();
private final ContentResolver contentResolver;
private Iterable<Item> filterItems(Iterable<Item> items, final Interval interval) {
final Predicate<Item> validBroadcast = MorePredicates.transformingPredicate(
ITEM_TO_BROADCAST_INTERVAL,
new ScheduleBroadcastFilter(interval)
);
return Iterables.transform(
ImmutableSet.copyOf(Iterables.transform(
Iterables.filter(items, validBroadcast),
ItemScheduleEntry.ITEM_SCHEDULE_ENTRY
)
),
ItemScheduleEntry.ITEM
);
}
private List<Item> filterLocations(Iterable<Item> items) {
return ImmutableList.copyOf(Iterables.transform(items, input -> {
for (Version version: input.getVersions()) {
for (Encoding encoding: version.getManifestedAs()) {
if (! encoding.getAvailableAt().isEmpty()) {
encoding.setAvailableAt(ImmutableSet.copyOf(Iterables.filter(
encoding.getAvailableAt(),
Location.AVAILABLE_LOCATION))
);
}
}
}
return input;
}));
}
private static List<String> keys(
Iterable<Interval> intervals,
Iterable<Channel> channels,
Iterable<Publisher> publishers
) {
ImmutableList.Builder<String> keys = ImmutableList.builder();
for (Interval interval: intervals) {
for (Channel channel: channels) {
for (Publisher publisher: publishers) {
keys.add(ScheduleEntry.toKey(interval, channel, publisher));
}
}
}
return keys.build();
}
private static class ItemScheduleEntry {
private final Item item;
public ItemScheduleEntry(Item item) {
Preconditions.checkNotNull(item);
this.item = item;
}
public Item item() {
return item;
}
public DateTime startTime() {
return ScheduleEntry.BROADCAST.apply(item).getTransmissionTime();
}
public String canonicalUri() {
return item.getCanonicalUri();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ItemScheduleEntry) {
ItemScheduleEntry entry = (ItemScheduleEntry) obj;
return startTime().equals(entry.startTime())
&& canonicalUri().equals(entry.canonicalUri()
);
}
return super.equals(obj);
}
@Override
public int hashCode() {
return Objects.hashCode(startTime(), canonicalUri());
}
final static Function<ItemScheduleEntry, Item> ITEM = ItemScheduleEntry::item;
final static Function<Item, ItemScheduleEntry> ITEM_SCHEDULE_ENTRY = ItemScheduleEntry::new;
}
void writeScheduleFrom(Item item1) {
writeScheduleFor(ImmutableList.of(item1));
}
}