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;
}
}
}
}
}