package org.atlasapi.messaging.v3; import java.io.IOException; import org.atlasapi.messaging.v3.ContentEquivalenceAssertionMessage.AdjacentRef; import org.atlasapi.messaging.worker.v3.AdjacentRefConfiguration; import org.atlasapi.messaging.worker.v3.ContentEquivalenceAssertionMessageConfiguration; import org.atlasapi.messaging.worker.v3.EntityUpdatedMessageConfiguration; import org.atlasapi.messaging.worker.v3.ScheduleUpdateMessageConfiguration; import org.atlasapi.serialization.json.JsonFactory; import com.metabroadcast.common.queue.Message; import com.metabroadcast.common.queue.MessageDeserializationException; import com.metabroadcast.common.queue.MessageSerializationException; import com.metabroadcast.common.queue.MessageSerializer; import com.metabroadcast.common.time.Timestamp; import com.fasterxml.jackson.core.JsonParseException; 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.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleDeserializers; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.guava.GuavaModule; import com.fasterxml.jackson.datatype.joda.JodaModule; import com.google.common.base.Objects; import static com.google.common.base.Preconditions.checkNotNull; public class JacksonMessageSerializer<M extends Message> implements MessageSerializer<M> { public static final <M extends Message> JacksonMessageSerializer<M> forType(Class<? extends M> cls) { return new JacksonMessageSerializer<>(cls); } /* This exists because 1. we made a boo-boo 2. there is now a message serialization kerfaffle The weird Timestamp class can be instantiated by both java.lang.Long and a long primitive, which really confuses Jackson (and confused a dev here at some point). So we made a boo-boo and broke serialization compatibility, and had - the old format, primitive {"@class": "... Timestamp", "millis": 42} - the new format, object {"@class": "... Timestamp", ["java.lang.Long", 42]} It's impossible to cope with both of those via just mixins, so we had to make a custom deserializer like this. */ public static class TimestampDeserializer extends JsonDeserializer<Timestamp> { @Override public Timestamp deserialize( JsonParser jp, DeserializationContext ctxt ) throws IOException { JsonToken token = jp.nextValue(); if (!"millis".equals(jp.getCurrentName())) { throw new JsonParseException("Expected millis field", jp.getCurrentLocation()); } Timestamp result; if (token == JsonToken.START_ARRAY) { JsonToken javaLangLongToken = jp.nextToken(); if (javaLangLongToken != JsonToken.VALUE_STRING) { throw new JsonParseException( "Expected string token with class name", jp.getCurrentLocation() ); } else if (!"java.lang.Long".equals(jp.getText())) { throw new JsonParseException( "Expected class name to be java.lang.Long", jp.getCurrentLocation() ); } JsonToken numberToken = jp.nextToken(); if (numberToken != JsonToken.VALUE_NUMBER_INT) { throw new JsonParseException( "Expected number token", jp.getCurrentLocation() ); } Long timestamp = jp.getLongValue(); JsonToken arrayCloseToken = jp.nextToken(); if (arrayCloseToken != JsonToken.END_ARRAY) { throw new JsonParseException( "Expected array end token", jp.getCurrentLocation() ); } result = Timestamp.of(timestamp); } else if (token == JsonToken.VALUE_NUMBER_INT) { result = Timestamp.of(jp.getValueAsLong()); } else { throw new JsonParseException( "Could not parse millis field", jp.getCurrentLocation() ); } token = jp.nextToken(); if (token != JsonToken.END_OBJECT) { throw new JsonParseException( "Expected end of object", jp.getCurrentLocation() ); } return result; } } public static class MessagingModule extends SimpleModule { public MessagingModule() { super("Messaging Module", new com.fasterxml.jackson.core.Version(0, 0, 1, null, null, null)); } @Override public void setupModule(SetupContext context) { super.setupModule(context); context.setMixInAnnotations( EntityUpdatedMessage.class, EntityUpdatedMessageConfiguration.class ); context.setMixInAnnotations( ContentEquivalenceAssertionMessage.class, ContentEquivalenceAssertionMessageConfiguration.class ); context.setMixInAnnotations(AdjacentRef.class, AdjacentRefConfiguration.class); SimpleDeserializers deserializers = new SimpleDeserializers(); deserializers.addDeserializer(Timestamp.class, new TimestampDeserializer()); context.addDeserializers(deserializers); context.setMixInAnnotations( ScheduleUpdateMessage.class, ScheduleUpdateMessageConfiguration.class ); } } private final ObjectMapper mapper = JsonFactory.makeJsonMapper() .registerModule(new MessagingModule()) .registerModule(new GuavaModule()) .registerModule(new JodaModule()); private final Class<? extends M> cls; public JacksonMessageSerializer(Class<? extends M> cls) { this.cls = checkNotNull(cls); } @Override public byte[] serialize(M message) throws MessageSerializationException { try { return mapper.writeValueAsBytes(message); } catch (IOException ioe) { throw new MessageSerializationException(message.toString(), ioe); } } @Override public M deserialize(byte[] serialized) throws MessageDeserializationException { try { return mapper.readValue(serialized, cls); } catch (IOException e) { throw new MessageDeserializationException(e); } } @Override public String toString() { return Objects.toStringHelper(getClass()) .addValue(cls.getSimpleName()) .toString(); } }