package org.atlasapi.persistence.content;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import com.metabroadcast.applications.client.model.internal.Application;
import org.atlasapi.media.entity.Content;
import org.atlasapi.media.entity.Identified;
import org.atlasapi.media.entity.LookupRef;
import org.atlasapi.media.entity.Publisher;
import org.atlasapi.output.Annotation;
import org.atlasapi.persistence.lookup.entry.LookupEntry;
import org.atlasapi.persistence.lookup.entry.LookupEntryStore;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
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.google.common.collect.Sets.SetView;
import com.metabroadcast.common.base.MorePredicates;
public class DefaultEquivalentContentResolver implements EquivalentContentResolver {
private KnownTypeContentResolver contentResolver;
private LookupEntryStore lookupResolver;
private final Ordering<LookupRef> nullSafeRefById = Ordering.natural().onResultOf(LookupRef.TO_ID).nullsLast();
public DefaultEquivalentContentResolver(
KnownTypeContentResolver contentResolver,
LookupEntryStore lookupResolver
) {
this.contentResolver = contentResolver;
this.lookupResolver = lookupResolver;
}
@Override
public EquivalentContent resolveUris(
Iterable<String> uris,
Application application,
Set<Annotation> activeAnnotations,
boolean withAliases
) {
Iterable<LookupEntry> entries = lookupResolver.entriesForIdentifiers(uris, withAliases);
return filterAndResolveEntries(ImmutableSet.copyOf(entries), uris, application);
}
@Override
public EquivalentContent resolveIds(
Iterable<Long> ids,
Application application,
Set<Annotation> activeAnnotations
) {
Iterable<LookupEntry> entries = lookupResolver.entriesForIds(ids);
Set<String> uris = Sets.newHashSet();
for (LookupEntry entry : entries) {
uris.add(entry.uri());
}
return filterAndResolveEntries(ImmutableSet.copyOf(entries), uris, application);
}
@Override
public EquivalentContent resolveAliases(
Optional<String> namespace,
Iterable<String> values,
Application application,
Set<Annotation> activeAnnotations
) {
Iterable<LookupEntry> entries = lookupResolver.entriesForAliases(namespace, values);
Set<String> uris = Sets.newHashSet();
for (LookupEntry entry : entries) {
uris.add(entry.uri());
}
return filterAndResolveEntries(ImmutableSet.copyOf(entries), uris, application);
}
protected EquivalentContent filterAndResolveEntries(Set<LookupEntry> entries, Iterable<String> uris, Application application) {
if (Iterables.isEmpty(entries)) {
return EquivalentContent.empty();
}
SetMultimap<String, LookupRef> uriToEquivs = byUri(subjsToEquivs(entries, application));
ImmutableSet<LookupRef> refs = ImmutableSet.copyOf(uriToEquivs.values());
if (refs.isEmpty()) {
return EquivalentContent.empty();
}
Map<String, LookupEntry> entryIndex = Maps.uniqueIndex(entries, LookupEntry.TO_ID);
ResolvedContent resolvedContent = contentResolver.findByLookupRefs(refs);
EquivalentContent.Builder equivalentContent = EquivalentContent.builder();
for (String uri : uris) {
Set<LookupRef> equivRefs = uriToEquivs.get(uri);
Iterable<Content> contents = equivContent(equivRefs, resolvedContent);
LookupEntry entry = entryIndex.get(uri);
if (entry != null) {
Set<LookupRef> allRefs = equivRefs(contents, entry, application);
for (Content content : contents) {
content.setEquivalentTo(allRefs);
}
}
equivalentContent.putEquivalents(uri, contents);
}
return equivalentContent.build();
}
private Set<LookupRef> equivRefs(Iterable<Content> contents, LookupEntry entry,
Application application) {
Iterable<LookupRef> enabled = Iterables.transform(contents, LookupRef.FROM_DESCRIBED);
Set<LookupRef> disabled = removeEnabledSources(entry.equivalents(), application);
return Sets.union(ImmutableSet.copyOf(enabled), disabled);
}
private Set<LookupRef> removeEnabledSources(Set<LookupRef> equivalents,
Application application) {
Predicate<Publisher> isDisabled = Predicates.not(Predicates.in(application.getConfiguration().getEnabledReadSources()));
return ImmutableSet.copyOf(Iterables.filter(equivalents,
MorePredicates.transformingPredicate(LookupRef.TO_SOURCE, isDisabled)));
}
private SetMultimap<String, LookupRef> byUri(SetMultimap<LookupEntry, LookupRef> subjsToEquivs) {
ImmutableSetMultimap.Builder<String, LookupRef> byUri = ImmutableSetMultimap.builder();
for (Entry<LookupEntry, Collection<LookupRef>> subjToEquivs : subjsToEquivs.asMap().entrySet()) {
byUri.putAll(subjToEquivs.getKey().uri(), subjToEquivs.getValue());
}
return byUri.build();
}
protected Iterable<Content> equivContent(Set<LookupRef> equivUris, ResolvedContent resolved) {
if (equivUris == null) {
return ImmutableSet.of();
}
List<Identified> resolvedEquivs = resolved.getResolvedResults(Iterables.transform(equivUris,LookupRef.TO_URI));
return Iterables.filter(resolvedEquivs, Content.class);
}
private SetMultimap<LookupEntry, LookupRef> subjsToEquivs(
Iterable<LookupEntry> resolved,
Application application
) {
Predicate<LookupRef> sourceFilter = MorePredicates.transformingPredicate(LookupRef.TO_SOURCE, Predicates.in(application.getConfiguration().getEnabledReadSources()));
SetMultimap<LookupRef, LookupRef> secondaryResolve = HashMultimap.create();
ImmutableSetMultimap.Builder<LookupEntry, LookupRef> subjsToEquivs = ImmutableSetMultimap.builder();
for (LookupEntry entry : resolved) {
Set<LookupRef> selectedEquivs = Sets.filter(entry.equivalents(), sourceFilter);
if (application.getConfiguration().isPrecedenceEnabled()) {
//ensure only one from precedent
LookupRef refToSave;
if (isPrecedentSourceEntry(entry, application)) {
refToSave = entry.lookupRef();
} else {
refToSave = lowestIdFromPrecedentSource(selectedEquivs, application);
}
if (refToSave != null) {
Iterable<LookupRef> toRemove = othersFromSourceOf(refToSave, selectedEquivs);
secondaryResolve.putAll(entry.lookupRef(), toRemove);
}
}
subjsToEquivs.putAll(entry, selectedEquivs);
}
return secondaryResolve.isEmpty() ? subjsToEquivs.build()
: resolveAndFilter(secondaryResolve, subjsToEquivs.build(), sourceFilter);
}
private ImmutableSetMultimap<LookupEntry, LookupRef> resolveAndFilter(
SetMultimap<LookupRef, LookupRef> secondaryResolve,
ImmutableSetMultimap<LookupEntry, LookupRef> subjsToEquivs,
Predicate<LookupRef> sourceFilter
) {
Map<LookupRef,LookupEntry> entriesToRemove = Maps.uniqueIndex(
lookupResolver.entriesForCanonicalUris(Iterables.transform(ImmutableSet.copyOf(secondaryResolve.values()), LookupRef.TO_URI)),
LookupEntry.TO_SELF
);
ImmutableSetMultimap.Builder<LookupEntry, LookupRef> filtered
= ImmutableSetMultimap.builder();
for (Entry<LookupEntry, Collection<LookupRef>> subjToEquivs : subjsToEquivs.asMap().entrySet()) {
Set<LookupRef> filteredEquivs = Sets.newHashSet(subjToEquivs.getValue());
Set<LookupRef> removalRefs = secondaryResolve.get(subjToEquivs.getKey().lookupRef());
//remove all the adjacent of the refs to remove.
for (LookupRef equiv : subjToEquivs.getValue()) {
if (removalRefs.contains(equiv)) {
LookupEntry entryToRemove = entriesToRemove.get(equiv);
if (entryToRemove != null) {
filteredEquivs.removeAll(allAdjacents(entryToRemove));
}
}
}
//ensure we always get the thing we actually asked for.
if (sourceFilter.apply(subjToEquivs.getKey().lookupRef())) {
filteredEquivs.add(subjToEquivs.getKey().lookupRef());
}
//and its exclusive enabled adjacent.
filteredEquivs.addAll(Sets.difference(
Sets.filter(allAdjacents(subjToEquivs.getKey()), sourceFilter),
secondaryResolve.get(subjToEquivs.getKey().lookupRef())
));
filtered.putAll(subjToEquivs.getKey(), filteredEquivs);
}
return filtered.build();
}
private SetView<LookupRef> allAdjacents(LookupEntry entry) {
return Sets.union(entry.directEquivalents(), entry.explicitEquivalents());
}
private LookupRef lowestIdFromPrecedentSource(
Set<LookupRef> selectedEquivs,
Application application
) {
LookupRef lowestId = null;
Publisher publisher = application.getConfiguration()
.getReadPrecedenceOrdering()
.sortedCopy(application.getConfiguration().getEnabledReadSources())
.get(0);
Iterable<LookupRef> lookupRefs = selectedEquivs.stream()
.filter(fromSource(publisher)::apply)
.collect(Collectors.toList());
for (LookupRef lookupRef : lookupRefs) {
lowestId = nullSafeRefById.min(lowestId, lookupRef);
}
return lowestId;
}
private Iterable<LookupRef> othersFromSourceOf(LookupRef saveRef, Set<LookupRef> selectedEquivs) {
return Iterables.filter(selectedEquivs, Predicates.and(Predicates.not(Predicates.equalTo(saveRef)),fromSource(saveRef.publisher())));
}
private Predicate<LookupRef> fromSource(Publisher src) {
return MorePredicates.transformingPredicate(LookupRef.TO_SOURCE,
Predicates.equalTo(src));
}
private boolean isPrecedentSourceEntry(LookupEntry entry, Application application) {
return application.getConfiguration()
.getReadPrecedenceOrdering()
.sortedCopy(application.getConfiguration().getEnabledReadSources())
.get(0)
.equals(entry.lookupRef().publisher());
}
}