package org.atlasapi.equiv;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.atlasapi.media.entity.Publisher;
import org.atlasapi.persistence.cassandra.CassandraPersistenceException;
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.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.reflect.TypeToken;
import com.metabroadcast.common.collect.ImmutableOptionalMap;
import com.metabroadcast.common.collect.OptionalMap;
import com.netflix.astyanax.AstyanaxContext;
import com.netflix.astyanax.ColumnListMutation;
import com.netflix.astyanax.Keyspace;
import com.netflix.astyanax.MutationBatch;
import com.netflix.astyanax.connectionpool.OperationResult;
import com.netflix.astyanax.model.Column;
import com.netflix.astyanax.model.ColumnFamily;
import com.netflix.astyanax.model.ConsistencyLevel;
import com.netflix.astyanax.model.Row;
import com.netflix.astyanax.model.Rows;
import com.netflix.astyanax.query.ColumnFamilyQuery;
import com.netflix.astyanax.query.RowSliceQuery;
import com.netflix.astyanax.serializers.StringSerializer;
public class CassandraEquivalenceSummaryStore implements EquivalenceSummaryStore {
private final class PublisherAdapter implements JsonSerializer<Publisher>, JsonDeserializer<Publisher> {
@Override
public JsonElement serialize(Publisher src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.key());
}
@Override
public Publisher deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
return Publisher.fromKey(json.getAsString()).requireValue();
}
}
private static final class EquivalenceSummaryDeserializer implements
JsonDeserializer<EquivalenceSummary> {
private static final TypeToken<PersistentEquivalencesContentRefs> equivalentContentRefsTypeToken = new TypeToken<PersistentEquivalencesContentRefs>(){};
private static final TypeToken<List<String>> candidatesTypeToken
= new TypeToken<List<String>>(){};
@Override
public EquivalenceSummary deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
String subject = obj.get("subject").getAsString();
String parent = deserializeParent(obj.get("parent"));
List<String> candidates = deserializeCandidates(context, obj.get("candidates"));
Multimap<Publisher,ContentRef> equivalents = deserializeEquivalents(context, obj.get("equivalents"));
return new EquivalenceSummary(subject, parent, candidates, equivalents);
}
private String deserializeParent(JsonElement parent) {
return parent == null ? null : parent.getAsString();
}
private Multimap<Publisher, ContentRef> deserializeEquivalents(
JsonDeserializationContext context, JsonElement equivs) {
JsonObject jsonObject = (JsonObject) equivs;
ImmutableMultimap.Builder<Publisher, ContentRef> builder = ImmutableMultimap.builder();
Set<Map.Entry<String, JsonElement>> entries = jsonObject.entrySet();
for (Map.Entry<String, JsonElement> entry : entries) {
PersistentEquivalencesContentRefs contentRefWithEquivs = deserializeContentRef(context, entry.getValue());
Publisher publisher = Publisher.fromKey(entry.getKey()).requireValue();
if(contentRefWithEquivs.getEquivalents() != null) {
for (ContentRef ref : contentRefWithEquivs.getEquivalents()) {
builder.put(publisher, ref);
}
} else {
ContentRef contentRef = new ContentRef(contentRefWithEquivs.getCanonicalUri(), contentRefWithEquivs.getPublisher(), contentRefWithEquivs.getParentUri());
builder.put(publisher,contentRef);
}
}
return builder.build();
}
private PersistentEquivalencesContentRefs deserializeContentRef(JsonDeserializationContext context,
JsonElement contentRef) {
return context.deserialize(contentRef, equivalentContentRefsTypeToken.getType());
}
private List<String> deserializeCandidates(JsonDeserializationContext context,
JsonElement candidates) {
return context.deserialize(candidates, candidatesTypeToken.getType());
}
}
private static final String SUMMARY_CF_NAME = "EquivalenceSummaries";
private static final String SUMMARY_COL = "summary";
private static final String PARENT_COL = "parent";
static final ColumnFamily<String, String> EQUIV_SUM_CF =
new ColumnFamily<String, String>(
SUMMARY_CF_NAME,
StringSerializer.get(),
StringSerializer.get()
);
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(EquivalenceSummary.class, new EquivalenceSummaryDeserializer())
.registerTypeAdapter(Publisher.class, new PublisherAdapter())
.create();
private final Keyspace keyspace;
private final int requestTimeout;
public CassandraEquivalenceSummaryStore(AstyanaxContext<Keyspace> context, int requestTimeout) {
this.keyspace = context.getEntity();
this.requestTimeout = requestTimeout;
}
@Override
public void store(EquivalenceSummary summary) {
try {
MutationBatch mutationBatch = keyspace.prepareMutationBatch();
mutationBatch.setConsistencyLevel(ConsistencyLevel.CL_QUORUM);
ColumnListMutation<String> mutation = mutationBatch
.withRow(EQUIV_SUM_CF, summary.getSubject())
.putColumn(SUMMARY_COL, serialize(summary), null);
if (summary.getParent() != null) {
mutation.putColumn(PARENT_COL, summary.getParent(), null);
}
Future<OperationResult<Void>> result = mutationBatch.executeAsync();
result.get(requestTimeout, TimeUnit.MILLISECONDS);
} catch (Exception e) {
throw new CassandraPersistenceException(summary.getSubject(), e);
}
}
private byte[] serialize(EquivalenceSummary summary) throws Exception {
ImmutableMultimap<Publisher, ContentRef> equivalents = summary.getEquivalents();
ImmutableMap.Builder<Publisher, PersistentEquivalencesContentRefs> builder = ImmutableMap.builder();
java.util.Optional<ContentRef> firstContentRef = equivalents.values().stream().findFirst();
if (firstContentRef.isPresent()) {
for (Map.Entry<Publisher, Collection<ContentRef>> entry : equivalents.asMap()
.entrySet()) {
PersistentEquivalencesContentRefs contentRefs = getEquivalentContentRefs(firstContentRef.get(), entry);
builder.put(entry.getKey(), contentRefs);
}
}
PersistenceEquivalenceSummary summaryWithMultimap =
new PersistenceEquivalenceSummary.Builder()
.withSubject(summary.getSubject())
.withParent(summary.getParent())
.withCandidates(summary.getCandidates())
.withEquivalents(builder.build()).build();
return gson.toJson(summaryWithMultimap).getBytes();
}
private PersistentEquivalencesContentRefs getEquivalentContentRefs(
ContentRef firstContentRef, Map.Entry<Publisher, Collection<ContentRef>> entry) {
return PersistentEquivalencesContentRefs.builder()
.withSubject(firstContentRef.getCanonicalUri())
.withParentUri(firstContentRef.getParentUri())
.withPublisher(firstContentRef.getPublisher())
.withEquivalents(ImmutableList.copyOf(entry.getValue()))
.build();
}
@Override
public OptionalMap<String, EquivalenceSummary> summariesForUris(Iterable<String> uris) {
return deserialize(uris, rowsForUris(uris));
}
private Rows<String, String> rowsForUris(Iterable<String> uris) {
try {
ColumnFamilyQuery<String, String> query = keyspace
.prepareQuery(EQUIV_SUM_CF)
.setConsistencyLevel(ConsistencyLevel.CL_QUORUM);
RowSliceQuery<String, String> slice = query.getKeySlice(ImmutableSet.copyOf(uris));
Future<OperationResult<Rows<String, String>>> queryResult = slice.executeAsync();
return queryResult.get(requestTimeout, TimeUnit.MILLISECONDS).getResult();
} catch (Exception e) {
throw new CassandraPersistenceException(e.getMessage(), e);
}
}
private OptionalMap<String, EquivalenceSummary> deserialize(Iterable<String> uris, Rows<String, String> result) {
Builder<String, Optional<EquivalenceSummary>> resultMap = ImmutableMap.builder();
for (String uri : uris) {
EquivalenceSummary value = deserialize(result.getRow(uri));
resultMap.put(uri, Optional.fromNullable(value));
}
return ImmutableOptionalMap.copyOf(resultMap.build());
}
private EquivalenceSummary deserialize(Row<String, String> row) {
try {
Column<String> column = row.getColumns().getColumnByName(SUMMARY_COL);
if (column == null) {
return null;
}
byte[] columnBytes = column.getByteArrayValue();
InputStreamReader reader = new InputStreamReader(new ByteArrayInputStream(columnBytes));
return gson.fromJson(reader, EquivalenceSummary.class);
} catch (Exception e) {
throw new CassandraPersistenceException(row.getKey(), e);
}
}
private static class PersistenceEquivalenceSummary {
private final String subject;
private final String parent;
private final ImmutableList<String> candidates;
private final ImmutableMap<Publisher, PersistentEquivalencesContentRefs> equivalents;
private PersistenceEquivalenceSummary(String subject, String parent,
ImmutableList<String> candidates,
ImmutableMap<Publisher, PersistentEquivalencesContentRefs> equivalents) {
this.subject = subject;
this.parent = parent;
this.candidates = candidates;
this.equivalents = equivalents;
}
public String getSubject() {
return subject;
}
public String getParent() {
return parent;
}
public ImmutableList<String> getCandidates() {
return candidates;
}
public ImmutableMap<Publisher, PersistentEquivalencesContentRefs> getEquivalents() {
return equivalents;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String subject;
private String parent;
private ImmutableList<String> candidates;
private ImmutableMap<Publisher, PersistentEquivalencesContentRefs> equivalents;
private Builder() {
}
public Builder withSubject(String subject) {
this.subject = subject;
return this;
}
public Builder withParent(String parent) {
this.parent = parent;
return this;
}
public Builder withCandidates(ImmutableList<String> candidates) {
this.candidates = candidates;
return this;
}
public Builder withEquivalents(
ImmutableMap<Publisher, PersistentEquivalencesContentRefs> equivalents) {
this.equivalents = equivalents;
return this;
}
public PersistenceEquivalenceSummary build() {
return new PersistenceEquivalenceSummary(subject, parent, candidates, equivalents);
}
}
}
private static class PersistentEquivalencesContentRefs {
private final String canonicalUri;
private final Publisher publisher;
private final String parentUri;
private final List<ContentRef> equivalents;
private PersistentEquivalencesContentRefs(String canonicalUri, Publisher publisher, String parentUri,
List<ContentRef> equivalents) {
this.canonicalUri = canonicalUri;
this.publisher = publisher;
this.parentUri = parentUri;
this.equivalents = equivalents;
}
public Publisher getPublisher() {
return publisher;
}
public String getParentUri() {
return parentUri;
}
public List<ContentRef> getEquivalents() {
return equivalents;
}
public String getCanonicalUri() {
return canonicalUri;
}
public static Builder builder() {
return new PersistentEquivalencesContentRefs.Builder();
}
public static final class Builder {
private String subject;
private Publisher publisher;
private String parentUri;
private List<ContentRef> equivalents;
private Builder() {
}
public Builder withSubject(String subject) {
this.subject = subject;
return this;
}
public Builder withParentUri(String parentUri) {
this.parentUri = parentUri;
return this;
}
public Builder withPublisher(Publisher publisher) {
this.publisher = publisher;
return this;
}
public Builder withEquivalents(List<ContentRef> equivalents) {
this.equivalents = equivalents;
return this;
}
public PersistentEquivalencesContentRefs build() {
return new PersistentEquivalencesContentRefs(subject, publisher, parentUri, equivalents);
}
}
}
}