package org.rakam.collection;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import com.google.common.primitives.Ints;
import org.rakam.analysis.ApiKeyService;
import org.rakam.util.RakamException;
import javax.inject.Inject;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static com.fasterxml.jackson.core.JsonToken.FIELD_NAME;
import static com.fasterxml.jackson.core.JsonToken.START_OBJECT;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.rakam.analysis.ApiKeyService.AccessKeyType.MASTER_KEY;
import static org.rakam.analysis.ApiKeyService.AccessKeyType.WRITE_KEY;
public class EventListDeserializer
extends JsonDeserializer<EventList>
{
private final JsonEventDeserializer eventDeserializer;
private final ApiKeyService apiKeyService;
@Inject
public EventListDeserializer(ApiKeyService apiKeyService,
JsonEventDeserializer jsonEventDeserializer)
{
eventDeserializer = jsonEventDeserializer;
this.apiKeyService = apiKeyService;
}
@Override
public EventList deserialize(JsonParser jp, DeserializationContext deserializationContext)
throws IOException
{
JsonToken t = jp.getCurrentToken();
if (t != START_OBJECT) {
throw new IllegalArgumentException("Body must be an object");
}
Event.EventContext context = null;
t = jp.nextToken();
if (t != FIELD_NAME) {
deserializationContext.reportWrongTokenException(jp, JsonToken.FIELD_NAME, null);
}
String fieldName = jp.getCurrentName();
jp.nextToken();
TokenBuffer eventsBuffer = null;
if (fieldName.equals("api")) {
context = jp.readValueAs(Event.EventContext.class);
}
else if (fieldName.equals("events")) {
InputStream stream = (InputStream) deserializationContext.getAttribute("stream");
eventsBuffer = jp.readValueAs(TokenBuffer.class);
}
else {
throw new RakamException(format("Invalid property '%s'", fieldName), BAD_REQUEST);
}
t = jp.nextToken();
if (t != FIELD_NAME) {
deserializationContext.reportWrongTokenException(jp, JsonToken.FIELD_NAME, null);
}
fieldName = jp.getCurrentName();
jp.nextToken();
if (fieldName.equals("api")) {
if (context != null) {
throw new RakamException("multiple 'api' property", BAD_REQUEST);
}
context = jp.readValueAs(Event.EventContext.class);
if (eventsBuffer == null) {
throw new IllegalStateException();
}
JsonParser eventJp = eventsBuffer.asParser(jp);
eventJp.nextToken();
return readEvents(eventJp, context, deserializationContext);
}
else if (fieldName.equals("events")) {
if (eventsBuffer != null) {
throw new RakamException("multiple 'events' property", BAD_REQUEST);
}
return readEvents(jp, context, deserializationContext);
}
else {
throw new RakamException(format("Invalid property '%s'", fieldName), BAD_REQUEST);
}
}
private EventList readEvents(JsonParser jp, Event.EventContext context, DeserializationContext deserializationContext)
throws IOException
{
Object inputSource = jp.getInputSource();
long start = jp.getTokenLocation().getByteOffset();
List<Event> list = new ArrayList<>();
if (jp.getCurrentToken() != JsonToken.START_ARRAY) {
throw new RakamException("events field must be array", BAD_REQUEST);
}
JsonToken t = jp.nextToken();
Object apiKey = deserializationContext.getAttribute("apiKey");
String project = null;
boolean masterKey = false;
if (apiKey == null || apiKey == WRITE_KEY) {
if (context == null) {
throw new RakamException("api parameter is required", BAD_REQUEST);
}
try {
project = apiKeyService.getProjectOfApiKey(context.apiKey,
apiKey == null ? WRITE_KEY : (ApiKeyService.AccessKeyType) apiKey);
}
catch (RakamException e) {
masterKey = true;
}
}
if (project == null) {
masterKey = true;
try {
project = apiKeyService.getProjectOfApiKey(context.apiKey, MASTER_KEY);
}
catch (RakamException e) {
if (e.getStatusCode() == FORBIDDEN) {
throw new RakamException("api_key is invalid", FORBIDDEN);
}
throw e;
}
}
for (; t == START_OBJECT; t = jp.nextToken()) {
list.add(eventDeserializer.deserializeWithProject(jp, project, context, masterKey));
}
long end = jp.getTokenLocation().getByteOffset();
if (context.checksum != null) {
Object sourceRef = jp.getTokenLocation().getSourceRef();
if (sourceRef instanceof byte[]) {
validateChecksum((byte[]) sourceRef, start, end, context);
}
}
return new EventList(context, project, list);
}
private void validateChecksum(byte[] sourceRef, long start, long end, Event.EventContext context)
{
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
}
catch (NoSuchAlgorithmException e) {
throw new RakamException(INTERNAL_SERVER_ERROR);
}
if (context.apiKey != null) {
md.update(context.apiKey.getBytes(UTF_8));
}
if (context.apiVersion != null) {
md.update(context.apiVersion.getBytes(UTF_8));
}
if (context.uploadTime != null) {
md.update(String.valueOf(context.uploadTime).getBytes(UTF_8));
}
md.update(sourceRef, Ints.checkedCast(start), Ints.checkedCast(end - start) + 1);
String md5 = DatatypeConverter.printHexBinary(md.digest());
if (!md5.equals(context.checksum.toUpperCase(Locale.ENGLISH))) {
throw new RakamException("Checksum is invalid", BAD_REQUEST);
}
}
}