ProtoMessageSerializer.java

/*
 * Copyright 2017 Providence Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package net.morimekta.proto.jackson.adapter;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Duration;
import com.google.protobuf.Message;
import com.google.protobuf.ProtocolMessageEnum;
import com.google.protobuf.Timestamp;
import net.morimekta.proto.MorimektaOptions;
import net.morimekta.proto.ProtoMessage;
import net.morimekta.proto.jackson.ProtoFeature;
import net.morimekta.proto.jackson.ProtoStringFeature;
import net.morimekta.proto.jackson.ProtoTypeRegistryOption;
import net.morimekta.proto.utils.JsonNameUtil;

import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;

import static net.morimekta.proto.utils.ByteStringUtil.toBase64;
import static net.morimekta.proto.utils.FieldUtil.getMapValueDescriptor;
import static net.morimekta.proto.utils.GoogleTypesUtil.simpleDurationString;
import static net.morimekta.proto.utils.GoogleTypesUtil.toInstant;
import static net.morimekta.proto.utils.JsonNameUtil.getJsonEnumName;
import static net.morimekta.proto.utils.ValueUtil.toJavaValue;

/**
 * Serializes proto messages.
 */
public class ProtoMessageSerializer
        extends StdSerializer<Message> {
    /**
     * Instantiate serializer.
     */
    public ProtoMessageSerializer() {
        super(Message.class);
    }

    @Override
    public void serialize(Message value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        serializeMessage(value, gen, serializers);
    }

    private static void serializeMessage(Message value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException {
        if (value instanceof Any && ProtoFeature.WRITE_UNPACKED_ANY.isEnabled(serializers)) {
            var tr = ProtoTypeRegistryOption.getRegistry(serializers);
            if (tr != null) {
                var any = (Any) value;
                var descriptor = tr.messageTypeByTypeUrl(any.getTypeUrl());
                if (descriptor != null) {
                    var type = ProtoMessage.getMessageClass(descriptor);
                    var unpacked = any.unpack(type);
                    gen.writeStartObject();
                    gen.writeFieldName(ProtoStringFeature.ANY_TYPE_FIELD_NAME.get(serializers));
                    gen.writeString(ProtoStringFeature.ANY_TYPE_PREFIX.get(serializers) + descriptor.getFullName());
                    writeMessageFields(unpacked, gen, serializers);
                    gen.writeEndObject();
                    return;
                }
            }
        } else if (value instanceof Timestamp && ProtoFeature.WRITE_TIMESTAMP_AS_ISO.isEnabled(serializers)) {
            var instant = toInstant((Timestamp) value);
            gen.writeString(DateTimeFormatter.ISO_INSTANT.format(instant));
            return;
        } else if (value instanceof Duration && ProtoFeature.WRITE_DURATION_AS_STRING.isEnabled(serializers)) {
            var durationStr = simpleDurationString((Duration) value);
            if (durationStr != null) {
                gen.writeString(durationStr);
                return;
            }
        }
        if (ProtoFeature.WRITE_COMPACT_MESSAGES.isEnabled(serializers) &&
            value.getDescriptorForType().getOptions().getExtension(MorimektaOptions.compact)) {
            ProtoMessage message = new ProtoMessage(value);
            gen.writeStartArray();
            int skipped = 0;
            for (Descriptors.FieldDescriptor field : message.getDescriptor().getFields()) {
                Optional<Object> val = message.optional(field);
                if (val.isPresent()) {
                    while (skipped > 0) {
                        gen.writeNull();
                        --skipped;
                    }
                    writeValue(val.get(), field, gen, serializers);
                } else {
                    ++skipped;
                }
            }
            gen.writeEndArray();
        } else {
            gen.writeStartObject();
            writeMessageFields(value, gen, serializers);
            gen.writeEndObject();
        }
    }

    private static void writeMessageFields(Message value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException {
        boolean fieldNamesAsIds = ProtoFeature.WRITE_FIELD_AS_NUMBER.isEnabled(serializers);
        for (var field : value.getAllFields().entrySet()) {
            Object val = toJavaValue(field.getKey(), field.getValue());
            if (fieldNamesAsIds) {
                gen.writeFieldId(field.getKey().getNumber());
            } else {
                gen.writeFieldName(JsonNameUtil.getJsonFieldName(field.getKey()));
            }
            writeValue(val, field.getKey(), gen, serializers);
        }
    }

    private static void writeValue(Object value,
                                   Descriptors.FieldDescriptor field,
                                   JsonGenerator gen,
                                   SerializerProvider serializers) throws IOException {
        if (value instanceof Collection) {
            @SuppressWarnings("unchecked")
            var collection = (Collection<Object>) value;
            gen.writeStartArray();
            for (Object val : collection) {
                writeValue(val, field, gen, serializers);
            }
            gen.writeEndArray();
        } else if (value instanceof Map) {
            var valueDescriptor = getMapValueDescriptor(field);
            @SuppressWarnings("unchecked")
            var objectMap = (Map<Object, Object>) value;
            gen.writeStartObject();
            for (var entry : objectMap.entrySet()) {
                gen.writeFieldName(entry.getKey().toString());
                writeValue(entry.getValue(), valueDescriptor, gen, serializers);
            }
            gen.writeEndObject();
        } else {
            switch (field.getType().getJavaType()) {
                case BOOLEAN:
                    gen.writeBoolean((Boolean) value);
                    break;
                case INT:
                    gen.writeNumber(((Number) value).intValue());
                    break;
                case LONG:
                    gen.writeNumber(((Number) value).longValue());
                    break;
                case FLOAT:
                    gen.writeNumber(((Number) value).floatValue());
                    break;
                case DOUBLE:
                    gen.writeNumber(((Number) value).doubleValue());
                    break;
                case STRING:
                    gen.writeString((String) value);
                    break;
                case BYTE_STRING:
                    gen.writeString(toBase64((ByteString) value));
                    break;
                case ENUM: {
                    var e = (ProtocolMessageEnum) toJavaValue(field, value);
                    if (ProtoFeature.WRITE_ENUM_AS_NUMBER.isEnabled(serializers)) {
                        gen.writeNumber(e.getNumber());
                    } else {
                        gen.writeString(getJsonEnumName(e));
                    }
                    break;
                }
                case MESSAGE: {
                    serializeMessage((Message) value, gen, serializers);
                    break;
                }
            }
        }
    }
}