All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.streamnative.pulsar.handlers.kop.schemaregistry.providers.protobuf.ProtobufSchemaUtils Maven / Gradle / Ivy

There is a newer version: 4.0.0.4
Show newest version
/**
 * Copyright (c) 2019 - 2024 StreamNative, Inc.. All Rights Reserved.
 */
/**
 * Licensed 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 io.streamnative.pulsar.handlers.kop.schemaregistry.providers.protobuf;

import static com.squareup.wire.schema.internal.UtilKt.MAX_TAG_VALUE;
import static io.streamnative.pulsar.handlers.kop.schemaregistry.providers.protobuf.ProtobufSchema.DEFAULT_LOCATION;
import static io.streamnative.pulsar.handlers.kop.schemaregistry.providers.protobuf.ProtobufSchema.transform;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.base.Ascii;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.util.JsonFormat;
import com.squareup.wire.Syntax;
import com.squareup.wire.schema.Field.Label;
import com.squareup.wire.schema.Location;
import com.squareup.wire.schema.ProtoType;
import com.squareup.wire.schema.internal.parser.EnumConstantElement;
import com.squareup.wire.schema.internal.parser.EnumElement;
import com.squareup.wire.schema.internal.parser.ExtendElement;
import com.squareup.wire.schema.internal.parser.ExtensionsElement;
import com.squareup.wire.schema.internal.parser.FieldElement;
import com.squareup.wire.schema.internal.parser.GroupElement;
import com.squareup.wire.schema.internal.parser.MessageElement;
import com.squareup.wire.schema.internal.parser.OneOfElement;
import com.squareup.wire.schema.internal.parser.OptionElement;
import com.squareup.wire.schema.internal.parser.OptionElement.Kind;
import com.squareup.wire.schema.internal.parser.ProtoFileElement;
import com.squareup.wire.schema.internal.parser.ReservedElement;
import com.squareup.wire.schema.internal.parser.RpcElement;
import com.squareup.wire.schema.internal.parser.ServiceElement;
import com.squareup.wire.schema.internal.parser.TypeElement;
import io.streamnative.pulsar.handlers.kop.schemaregistry.providers.protobuf.diff.Context;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import kotlin.Pair;
import kotlin.ranges.IntRange;
import org.apache.commons.lang3.math.NumberUtils;

public class ProtobufSchemaUtils {

    private static final ObjectMapper jsonMapper = new ObjectMapper();

    private static final ObjectMapper mapperWithProtoFileDeserializer = new ObjectMapper();
    private static final SimpleModule module = new SimpleModule();

    static {
        module.addDeserializer(ProtoFileElement.class, new ProtoFileElementDeserializer());
        mapperWithProtoFileDeserializer.registerModule(module);
    }

    public static ProtobufSchema getSchema(Message message) {
        return message != null ? new ProtobufSchema(message.getDescriptorForType()) : null;
    }

    public static Object toObject(JsonNode value, ProtobufSchema schema) throws IOException {
        StringWriter out = new StringWriter();
        jsonMapper.writeValue(out, value);
        return toObject(out.toString(), schema);
    }

    public static Object toObject(String value, ProtobufSchema schema)
        throws InvalidProtocolBufferException {
        DynamicMessage.Builder message = schema.newMessageBuilder();
        JsonFormat.parser().merge(value, message);
        return message.build();
    }

    public static byte[] toJson(Message message) throws IOException {
        if (message == null) {
            return null;
        }
        String jsonString = JsonFormat.printer()
            .includingDefaultValueFields()
            .omittingInsignificantWhitespace()
            .print(message);
        return jsonString.getBytes(StandardCharsets.UTF_8);
    }

    protected static String toNormalizedString(ProtobufSchema schema) {
        FormatContext ctx = new FormatContext(false, true);
        return toFormattedString(ctx, schema);
    }

    protected static String toFormattedString(FormatContext ctx, ProtobufSchema schema) {
        if (ctx.normalize()) {
            ctx.collectTypeInfo(schema, true);
        }
        return toString(ctx, schema.rawSchema());
    }

    protected static String toString(ProtoFileElement protoFile) {
        FormatContext ctx = new FormatContext(false, false);
        return toString(ctx, protoFile);
    }

    private static String toString(FormatContext ctx, ProtoFileElement protoFile) {
        StringBuilder sb = new StringBuilder();
        if (protoFile.getSyntax() != null) {
            if (!ctx.normalize() || protoFile.getSyntax() == Syntax.PROTO_3) {
                sb.append("syntax = \"");
                sb.append(protoFile.getSyntax());
                sb.append("\";\n");
            }
        }
        if (protoFile.getPackageName() != null) {
            sb.append("package ");
            sb.append(protoFile.getPackageName());
            sb.append(";\n");
        }
        if (!protoFile.getImports().isEmpty() || !protoFile.getPublicImports().isEmpty()) {
            sb.append('\n');
            List imports = protoFile.getImports();
            if (ctx.normalize()) {
                imports = imports.stream().sorted().distinct().collect(Collectors.toList());
            }
            for (String file : imports) {
                sb.append("import \"");
                sb.append(file);
                sb.append("\";\n");
            }
            List publicImports = protoFile.getPublicImports();
            if (ctx.normalize()) {
                publicImports = publicImports.stream().sorted().distinct().collect(Collectors.toList());
            }
            for (String file : publicImports) {
                sb.append("import public \"");
                sb.append(file);
                sb.append("\";\n");
            }
        }
        List options = ctx.filterOptions(protoFile.getOptions());
        if (!options.isEmpty()) {
            sb.append('\n');
            for (OptionElement option : options) {
                sb.append(toOptionString(ctx, option));
            }
        }
        List types = filterTypes(ctx, protoFile.getTypes());
        if (!types.isEmpty()) {
            sb.append('\n');
            // Order of message types is significant since the client is using
            // the non-normalized schema to serialize message indexes
            for (TypeElement typeElement : types) {
                if (typeElement instanceof MessageElement) {
                    try (Context.NamedScope nameScope = ctx.enterName(typeElement.getName())) {
                        sb.append(toString(ctx, (MessageElement) typeElement));
                    }
                }
            }
            for (TypeElement typeElement : types) {
                if (typeElement instanceof EnumElement) {
                    try (Context.NamedScope nameScope = ctx.enterName(typeElement.getName())) {
                        sb.append(toString(ctx, (EnumElement) typeElement));
                    }
                }
            }
        }
        if (!ctx.ignoreExtensions() && !protoFile.getExtendDeclarations().isEmpty()) {
            sb.append('\n');
            List extendElems = protoFile.getExtendDeclarations();
            if (ctx.normalize()) {
                extendElems = extendElems.stream()
                    .flatMap(e -> e.getFields().stream().map(f -> new Pair<>(resolve(ctx, e.getName()), f)))
                    .collect(Collectors.groupingBy(
                        Pair::getFirst,
                        LinkedHashMap::new,  // deterministic order
                        Collectors.mapping(Pair::getSecond, Collectors.toList()))
                    )
                    .entrySet()
                    .stream()
                    .map(e -> new ExtendElement(DEFAULT_LOCATION, e.getKey(), "", e.getValue()))
                    .collect(Collectors.toList());
            }
            for (ExtendElement extendElem : extendElems) {
                sb.append(toString(ctx, extendElem));
            }
        }
        if (!protoFile.getServices().isEmpty()) {
            sb.append('\n');
            // Don't sort service elements to be consistent with the fact that
            // we don't sort message/enum elements
            for (ServiceElement service : protoFile.getServices()) {
                sb.append(toString(ctx, service));
            }
        }
        return sb.toString();
    }

    private static String toString(FormatContext ctx, ServiceElement service) {
        StringBuilder sb = new StringBuilder();
        sb.append("service ");
        sb.append(service.getName());
        sb.append(" {");
        List options = ctx.filterOptions(service.getOptions());
        if (!options.isEmpty()) {
            sb.append('\n');
            for (OptionElement option : options) {
                appendIndented(sb, toOptionString(ctx, option));
            }
        }
        if (!service.getRpcs().isEmpty()) {
            sb.append('\n');
            // Don't sort rpc elements to be consistent with the fact that
            // we don't sort message/enum elements
            for (RpcElement rpc : service.getRpcs()) {
                appendIndented(sb, toString(ctx, rpc));
            }
        }
        sb.append("}\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, RpcElement rpc) {
        StringBuilder sb = new StringBuilder();
        sb.append("rpc ");
        sb.append(rpc.getName());
        sb.append(" (");

        if (rpc.getRequestStreaming()) {
            sb.append("stream ");
        }
        String requestType = rpc.getRequestType();
        if (ctx.normalize()) {
            requestType = resolve(ctx, requestType);
        }
        sb.append(requestType);
        sb.append(") returns (");

        if (rpc.getResponseStreaming()) {
            sb.append("stream ");
        }
        String responseType = rpc.getResponseType();
        if (ctx.normalize()) {
            responseType = resolve(ctx, responseType);
        }
        sb.append(responseType);
        sb.append(")");

        List options = ctx.filterOptions(rpc.getOptions());
        if (!options.isEmpty()) {
            sb.append(" {\n");
            for (OptionElement option : options) {
                appendIndented(sb, toOptionString(ctx, option));
            }
            sb.append('}');
        }

        sb.append(";\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, EnumElement type) {
        StringBuilder sb = new StringBuilder();
        sb.append("enum ");
        sb.append(type.getName());
        sb.append(" {");

        if (!type.getReserveds().isEmpty()) {
            sb.append('\n');
            List reserveds = type.getReserveds();
            if (ctx.normalize()) {
                reserveds = reserveds.stream()
                    .flatMap(r -> r.getValues().stream()
                        .map(o -> new ReservedElement(
                            r.getLocation(),
                            r.getDocumentation(),
                            Collections.singletonList(o))
                        )
                    )
                    .collect(Collectors.toList());
                Comparator cmp = Comparator.comparing(r -> {
                    Object o = ((ReservedElement) r).getValues().get(0);
                    if (o instanceof IntRange) {
                        return ((IntRange) o).getStart();
                    } else if (o instanceof Integer) {
                        return (Integer) o;
                    } else {
                        return Integer.MAX_VALUE;
                    }
                }).thenComparing(r -> ((ReservedElement) r).getValues().get(0).toString());
                reserveds.sort(cmp);
            }
            for (ReservedElement reserved : reserveds) {
                appendIndented(sb, toString(ctx, reserved));
            }
        }

        if (type.getReserveds().isEmpty()
            && (!type.getOptions().isEmpty() || !type.getConstants().isEmpty())) {
            sb.append('\n');
        }

        List options = ctx.filterOptions(type.getOptions());
        if (!options.isEmpty()) {
            for (OptionElement option : options) {
                appendIndented(sb, toOptionString(ctx, option));
            }
        }
        if (!type.getConstants().isEmpty()) {
            List constants = type.getConstants();
            if (ctx.normalize()) {
                constants = new ArrayList<>(constants);
                constants.sort(Comparator
                    .comparing(EnumConstantElement::getTag)
                    .thenComparing(EnumConstantElement::getName));
            }
            for (EnumConstantElement constant : constants) {
                appendIndented(sb, toString(ctx, constant));
            }
        }
        sb.append("}\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, EnumConstantElement type) {
        StringBuilder sb = new StringBuilder();
        sb.append(type.getName());
        sb.append(" = ");
        sb.append(type.getTag());

        List options = ctx.filterOptions(type.getOptions());
        if (!options.isEmpty()) {
            sb.append(" ");
            appendOptions(ctx, sb, options);
        }
        sb.append(";\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, MessageElement type) {
        StringBuilder sb = new StringBuilder();
        sb.append("message ");
        sb.append(type.getName());
        sb.append(" {");

        if (!type.getReserveds().isEmpty()) {
            sb.append('\n');
            List reserveds = type.getReserveds();
            if (ctx.normalize()) {
                reserveds = reserveds.stream()
                    .flatMap(r -> r.getValues().stream()
                        .map(o -> new ReservedElement(
                            r.getLocation(),
                            r.getDocumentation(),
                            Collections.singletonList(o))
                        )
                    )
                    .collect(Collectors.toList());
                Comparator cmp = Comparator.comparing(r -> {
                    Object o = ((ReservedElement) r).getValues().get(0);
                    if (o instanceof IntRange) {
                        return ((IntRange) o).getStart();
                    } else if (o instanceof Integer) {
                        return (Integer) o;
                    } else {
                        return Integer.MAX_VALUE;
                    }
                }).thenComparing(r -> ((ReservedElement) r).getValues().get(0).toString());
                reserveds.sort(cmp);
            }
            for (ReservedElement reserved : reserveds) {
                appendIndented(sb, toString(ctx, reserved));
            }
        }
        List options = ctx.filterOptions(type.getOptions());
        if (!options.isEmpty()) {
            sb.append('\n');
            for (OptionElement option : options) {
                appendIndented(sb, toOptionString(ctx, option));
            }
        }
        if (!type.getFields().isEmpty()) {
            sb.append('\n');
            List fields = type.getFields();
            if (ctx.normalize()) {
                fields = new ArrayList<>(fields);
                fields.sort(Comparator.comparing(FieldElement::getTag));
            }
            for (FieldElement field : fields) {
                appendIndented(sb, toString(ctx, field));
            }
        }
        if (!type.getOneOfs().isEmpty()) {
            sb.append('\n');
            List oneOfs = type.getOneOfs();
            if (ctx.normalize()) {
                oneOfs = oneOfs.stream()
                    .filter(o -> !o.getFields().isEmpty())
                    .map(o -> {
                        List fields = new ArrayList<>(o.getFields());
                        fields.sort(Comparator.comparing(FieldElement::getTag));
                        return new OneOfElement(o.getName(), o.getDocumentation(),
                            fields, o.getGroups(), o.getOptions(), Location.get("base"));
                    })
                    .collect(Collectors.toList());
                oneOfs.sort(Comparator.comparing(o -> o.getFields().get(0).getTag()));
            }
            for (OneOfElement oneOf : oneOfs) {
                appendIndented(sb, toString(ctx, oneOf));
            }
        }
        if (!type.getGroups().isEmpty()) {
            sb.append('\n');
            List groups = type.getGroups();
            if (ctx.normalize()) {
                groups = new ArrayList<>(groups);
                groups.sort(Comparator.comparing(GroupElement::getTag));
            }
            for (GroupElement group : groups) {
                appendIndented(sb, toString(ctx, group));
            }
        }
        if (!ctx.ignoreExtensions() && !type.getExtensions().isEmpty()) {
            sb.append('\n');
            List extensions = type.getExtensions();
            if (ctx.normalize()) {
                extensions = extensions.stream()
                    .flatMap(r -> r.getValues().stream()
                        .map(o -> new ExtensionsElement(
                            r.getLocation(),
                            r.getDocumentation(),
                            Collections.singletonList(o))
                        )
                    )
                    .collect(Collectors.toList());
                Comparator cmp = Comparator.comparing(r -> {
                    Object o = ((ExtensionsElement) r).getValues().get(0);
                    if (o instanceof IntRange) {
                        return ((IntRange) o).getStart();
                    } else if (o instanceof Integer) {
                        return (Integer) o;
                    } else {
                        return Integer.MAX_VALUE;
                    }
                });
                extensions.sort(cmp);
            }
            for (ExtensionsElement extension : extensions) {
                appendIndented(sb, toString(ctx, extension));
            }
        }
        if (!ctx.ignoreExtensions() && !type.getExtendDeclarations().isEmpty()) {
            sb.append('\n');
            List extendElems = type.getExtendDeclarations();
            if (ctx.normalize()) {
                extendElems = extendElems.stream()
                    .flatMap(e -> e.getFields().stream().map(f -> new Pair<>(resolve(ctx, e.getName()), f)))
                    .collect(Collectors.groupingBy(
                        Pair::getFirst,
                        LinkedHashMap::new,  // deterministic order
                        Collectors.mapping(Pair::getSecond, Collectors.toList()))
                    )
                    .entrySet()
                    .stream()
                    .map(e -> new ExtendElement(DEFAULT_LOCATION, e.getKey(), "", e.getValue()))
                    .collect(Collectors.toList());
            }
            for (ExtendElement extendElem : extendElems) {
                appendIndented(sb, toString(ctx, extendElem));
            }
        }
        List types = filterTypes(ctx, type.getNestedTypes());
        if (!types.isEmpty()) {
            sb.append('\n');
            // Order of message types is significant since the client is using
            // the non-normalized schema to serialize message indexes
            for (TypeElement typeElement : types) {
                if (typeElement instanceof MessageElement) {
                    try (Context.NamedScope nameScope = ctx.enterName(typeElement.getName())) {
                        appendIndented(sb, toString(ctx, (MessageElement) typeElement));
                    }
                }
            }
            for (TypeElement typeElement : types) {
                if (typeElement instanceof EnumElement) {
                    try (Context.NamedScope nameScope = ctx.enterName(typeElement.getName())) {
                        appendIndented(sb, toString(ctx, (EnumElement) typeElement));
                    }
                }
            }
        }
        sb.append("}\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, ReservedElement type) {
        StringBuilder sb = new StringBuilder();
        sb.append("reserved ");

        boolean first = true;
        for (Object value : type.getValues()) {
            if (first) {
                first = false;
            } else {
                sb.append(", ");
            }
            if (value instanceof String) {
                sb.append("\"");
                sb.append(value);
                sb.append("\"");
            } else if (value instanceof Integer) {
                sb.append(value);
            } else if (value instanceof IntRange range) {
                if (ctx.normalize() && range.getStart().equals(range.getEndInclusive())) {
                    sb.append(range.getStart());
                } else {
                    sb.append(range.getStart());
                    sb.append(" to ");
                    int last = range.getEndInclusive();
                    if (last < MAX_TAG_VALUE) {
                        sb.append(last);
                    } else {
                        sb.append("max");
                    }
                }
            } else {
                throw new IllegalArgumentException();
            }
        }
        sb.append(";\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, ExtensionsElement type) {
        StringBuilder sb = new StringBuilder();
        sb.append("extensions ");

        boolean first = true;
        for (Object value : type.getValues()) {
            if (first) {
                first = false;
            } else {
                sb.append(", ");
            }
            if (value instanceof Integer) {
                sb.append(value);
            } else if (value instanceof IntRange range) {
                if (ctx.normalize() && range.getStart().equals(range.getEndInclusive())) {
                    sb.append(range.getStart());
                } else {
                    sb.append(range.getStart());
                    sb.append(" to ");
                    int last = range.getEndInclusive();
                    if (last < MAX_TAG_VALUE) {
                        sb.append(last);
                    } else {
                        sb.append("max");
                    }
                }
            } else {
                throw new IllegalArgumentException();
            }
        }
        sb.append(";\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, OneOfElement type) {
        StringBuilder sb = new StringBuilder();
        sb.append("oneof ");
        sb.append(type.getName());
        sb.append(" {");

        List options = ctx.filterOptions(type.getOptions());
        if (!options.isEmpty()) {
            sb.append('\n');
            for (OptionElement option : options) {
                appendIndented(sb, toOptionString(ctx, option));
            }
        }
        if (!type.getFields().isEmpty()) {
            sb.append('\n');
            // Fields have already been sorted while sorting oneOfs in the calling method
            List fields = type.getFields();
            for (FieldElement field : fields) {
                appendIndented(sb, toString(ctx, field));
            }
        }
        if (!type.getGroups().isEmpty()) {
            sb.append('\n');
            List groups = type.getGroups();
            if (ctx.normalize()) {
                groups = new ArrayList<>(groups);
                groups.sort(Comparator.comparing(GroupElement::getTag));
            }
            for (GroupElement group : groups) {
                appendIndented(sb, toString(ctx, group));
            }
        }
        sb.append("}\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, GroupElement group) {
        StringBuilder sb = new StringBuilder();
        Label label = group.getLabel();
        if (label != null) {
            sb.append(label.name().toLowerCase(Locale.US));
            sb.append(" ");
        }
        sb.append("group ");
        sb.append(group.getName());
        sb.append(" = ");
        sb.append(group.getTag());
        sb.append(" {");
        if (!group.getFields().isEmpty()) {
            sb.append('\n');
            List fields = group.getFields();
            if (ctx.normalize()) {
                fields = new ArrayList<>(fields);
                fields.sort(Comparator.comparing(FieldElement::getTag));
            }
            for (FieldElement field : fields) {
                appendIndented(sb, toString(ctx, field));
            }
        }
        sb.append("}\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, ExtendElement extendElem) {
        StringBuilder sb = new StringBuilder();
        sb.append("extend ");
        // Names have been resolved when grouping by name
        String extendName = extendElem.getName();
        sb.append(extendName);
        sb.append(" {");
        if (!extendElem.getFields().isEmpty()) {
            sb.append('\n');
            List fields = extendElem.getFields();
            if (ctx.normalize()) {
                fields = new ArrayList<>(fields);
                fields.sort(Comparator.comparing(FieldElement::getTag));
            }
            for (FieldElement field : fields) {
                appendIndented(sb, toString(ctx, field));
            }
        }
        sb.append("}\n");
        return sb.toString();
    }

    private static String toString(FormatContext ctx, FieldElement field) {
        StringBuilder sb = new StringBuilder();
        Label label = field.getLabel();
        String fieldType = field.getType();
        ProtoType fieldProtoType = ProtoType.get(fieldType);
        if (ctx.normalize()) {
            if (!fieldProtoType.isScalar() && !fieldProtoType.isMap()) {
                // See if the fieldType resolves to a message representing a map
                fieldType = resolve(ctx, fieldType);
                Context.TypeElementInfo typeInfo = ctx.getTypeForFullName(fieldType, true);
                if (typeInfo != null && typeInfo.isMap()) {
                    fieldProtoType = typeInfo.getMapType();
                } else {
                    fieldProtoType = ProtoType.get(fieldType);
                }
            }
            ProtoType mapValueType = fieldProtoType.getValueType();
            if (fieldProtoType.isMap() && mapValueType != null) {
                // Ensure the value of the map is fully resolved
                String valueType = ctx.resolve(mapValueType.toString(), true);
                if (valueType != null) {
                    fieldProtoType = ProtoType.get(
                        // Note we add a leading dot to valueType
                        "map<" + fieldProtoType.getKeyType() + ", ." + valueType + ">"
                    );
                }
                label = null;  // don't emit label for map
            }
            fieldType = fieldProtoType.toString();
        }
        if (label != null) {
            sb.append(label.name().toLowerCase(Locale.US));
            sb.append(" ");
        }
        sb.append(fieldType);
        sb.append(" ");
        sb.append(field.getName());
        sb.append(" = ");
        sb.append(field.getTag());

        List optionsWithSpecialValues = new ArrayList<>(field.getOptions());
        String defaultValue = field.getDefaultValue();
        if (defaultValue != null) {
            optionsWithSpecialValues.add(
                OptionElement.Companion.create("default", toKind(fieldProtoType), defaultValue));
        }
        String jsonName = field.getJsonName();
        if (jsonName != null) {
            optionsWithSpecialValues.add(
                OptionElement.Companion.create("json_name", Kind.STRING, jsonName));
        }
        optionsWithSpecialValues = ctx.filterOptions(optionsWithSpecialValues);
        if (!optionsWithSpecialValues.isEmpty()) {
            sb.append(" ");
            appendOptions(ctx, sb, optionsWithSpecialValues);
        }

        sb.append(";\n");
        return sb.toString();
    }

    @SuppressWarnings("unchecked")
    private static String toString(FormatContext ctx, OptionElement option) {
        StringBuilder sb = new StringBuilder();
        String name = option.getName();
        if (option.isParenthesized()) {
            sb.append("(").append(name).append(")");
        } else {
            sb.append(name);
        }
        Object value = option.getValue();
        switch (option.getKind()) {
            case STRING:
                sb.append(" = \"");
                sb.append(escapeChars(value.toString()));
                sb.append("\"");
                break;
            case BOOLEAN:
            case ENUM:
                sb.append(" = ");
                sb.append(value);
                break;
            case NUMBER:
                sb.append(" = ");
                sb.append(formatNumber(ctx, value));
                break;
            case OPTION:
                sb.append(".");
                // Treat nested options as non-parenthesized always, prevents double parentheses.
                sb.append(toString(ctx, (OptionElement) value));
                break;
            case MAP:
                sb.append(" = {\n");
                formatOptionMap(ctx, sb, (Map) value);
                sb.append('}');
                break;
            case LIST:
                sb.append(" = ");
                appendOptions(ctx, sb, (List) value);
                break;
            default:
                break;
        }
        return sb.toString();
    }

    private static List filterTypes(FormatContext ctx, List types) {
        if (ctx.normalize()) {
            return types.stream()
                .filter(type -> {
                    if (type instanceof MessageElement) {
                        Context.TypeElementInfo typeInfo = ctx.getType(type.getName(), true);
                        // Don't emit synthetic map message
                        return typeInfo == null || !typeInfo.isMap();
                    } else {
                        return true;
                    }
                })
                .collect(Collectors.toList());
        } else {
            return types;
        }
    }

    private static void formatOptionMap(
        FormatContext ctx, StringBuilder sb, Map valueMap) {
        if (ctx.normalize()) {
            valueMap = valueMap.entrySet().stream()
                .filter(e -> {
                    Object value = e.getValue();
                    return !(value instanceof List) || !((List) value).isEmpty();
                })
                .map(e -> {
                    String key = e.getKey();
                    Object value = e.getValue();
                    if (key.startsWith("[") && key.endsWith("]")) {
                        // Found an extension field
                        String fieldName = key.substring(1, key.length() - 1);
                        String resolved = ctx.resolve(ctx::getExtendFieldForFullName, fieldName, true);
                        if (resolved != null) {
                            return new Pair<>("[" + resolved + "]", value);
                        }
                    }
                    return new Pair<>(key, value);
                })
                .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond,
                    (e1, e2) -> e1, TreeMap::new));
        }
        int lastIndex = valueMap.size() - 1;
        int index = 0;
        for (Map.Entry entry : valueMap.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            String endl = index != lastIndex ? "," : "";
            String kv = key
                + ": "
                + formatOptionMapOrListValue(ctx, value)
                + endl;
            appendIndented(sb, kv);
            index++;
        }
    }

    @SuppressWarnings("unchecked")
    private static String formatOptionMapOrListValue(FormatContext ctx, Object value) {
        StringBuilder sb = new StringBuilder();
        if (value instanceof String) {
            sb.append("\"");
            sb.append(escapeChars(value.toString()));
            sb.append("\"");
        } else if (value instanceof Map) {
            sb.append("{\n");
            formatOptionMap(ctx, sb, (Map) value);
            sb.append('}');
        } else if (value instanceof List) {
            List list = (List) value;
            if (ctx.normalize() && list.size() == 1) {
                sb.append(formatOptionMapOrListValue(ctx, list.get(0)));
            } else {
                sb.append("[\n");
                int lastIndex = list.size() - 1;
                for (int i = 0; i < list.size(); i++) {
                    String endl = i != lastIndex ? "," : "";
                    String v = formatOptionMapOrListValue(ctx, list.get(i))
                        + endl;
                    appendIndented(sb, v);
                }
                sb.append("]");
            }
        } else if (value instanceof OptionElement.OptionPrimitive primitive) {
            switch (primitive.getKind()) {
                case BOOLEAN:
                case ENUM:
                    sb.append(primitive.getValue());
                    break;
                case NUMBER:
                    sb.append(formatNumber(ctx, primitive.getValue()));
                    break;
                default:
                    sb.append(formatOptionMapOrListValue(ctx, primitive.getValue()));
            }
        } else if (value instanceof OptionElement) {
            sb.append(toString(ctx, (OptionElement) value));
        } else {
            sb.append(value);
        }
        return sb.toString();
    }

    private static String toOptionString(FormatContext ctx, OptionElement option) {
        String sb = "option "
            + toString(ctx, option)
            + ";\n";
        return sb;
    }

    private static void appendOptions(
        FormatContext ctx, StringBuilder sb, List options) {
        int count = options.size();
        if (count == 1) {
            sb.append('[')
                .append(formatOptionMapOrListValue(ctx, options.get(0)))
                .append(']');
            return;
        }
        sb.append("[\n");
        for (int i = 0; i < count; i++) {
            String endl = i < count - 1 ? "," : "";
            appendIndented(sb, formatOptionMapOrListValue(ctx, options.get(i)) + endl);
        }
        sb.append(']');
    }

    private static Kind toKind(ProtoType protoType) {
        switch (protoType.getSimpleName()) {
            case "bool":
                return Kind.BOOLEAN;
            case "string":
                return Kind.STRING;
            case "bytes":
            case "double":
            case "float":
            case "fixed32":
            case "fixed64":
            case "int32":
            case "int64":
            case "sfixed32":
            case "sfixed64":
            case "sint32":
            case "sint64":
            case "uint32":
            case "uint64":
                return Kind.NUMBER;
            default:
                return Kind.ENUM;
        }
    }

    private static void appendIndented(StringBuilder sb, String value) {
        List lines = Arrays.asList(value.split("\n"));
        if (lines.size() > 1 && lines.get(lines.size() - 1).isEmpty()) {
            lines.remove(lines.size() - 1);
        }
        for (String line : lines) {
            sb.append("  ")
                .append(line)
                .append('\n');
        }
    }

    public static String escapeChars(String input) {
        StringBuilder buffer = new StringBuilder(input.length());
        for (int i = 0; i < input.length(); i++) {
            char curr = input.charAt(i);
            switch (curr) {
                case Ascii.BEL:
                    buffer.append("\\a");
                    break;
                case Ascii.BS:
                    buffer.append("\\b");
                    break;
                case Ascii.FF:
                    buffer.append("\\f");
                    break;
                case Ascii.NL:
                    buffer.append("\\n");
                    break;
                case Ascii.CR:
                    buffer.append("\\r");
                    break;
                case Ascii.HT:
                    buffer.append("\\t");
                    break;
                case Ascii.VT:
                    buffer.append("\\v");
                    break;
                case '\\':
                    buffer.append("\\\\");
                    break;
                case '\'':
                    buffer.append("\\'");
                    break;
                case '\"':
                    buffer.append("\\\"");
                    break;
                default:
                    buffer.append(curr);
            }
        }
        return buffer.toString();
    }

    private static String formatNumber(FormatContext formatContext, Object value) {
        if (formatContext.normalize()) {
            try {
                Number num;
                if (value instanceof Number) {
                    num = (Number) value;
                } else {
                    num = formatContext.parseNumber(value.toString());
                }
                value = formatContext.formatNumber(num);
            } catch (NumberFormatException e) {
                // ignore, could be -inf or nan for example
            }
        }
        return value.toString();
    }

    private static String resolve(Context ctx, String type) {
        String resolved = ctx.resolve(type, true);
        if (resolved == null) {
            throw new IllegalArgumentException("Could not resolve type: " + type);
        }
        return "." + resolved;
    }

    public static class FormatContext extends Context {
        private final boolean ignoreExtensions;
        private final boolean normalize;
        private NumberFormat numberFormat;

        public FormatContext(boolean ignoreExtensions, boolean normalize) {
            super();
            this.ignoreExtensions = ignoreExtensions;
            this.normalize = normalize;
        }

        public boolean ignoreExtensions() {
            return ignoreExtensions;
        }

        public boolean normalize() {
            return normalize;
        }

        public String formatNumber(Number number) {
            return numberFormat().format(number);
        }

        public Number parseNumber(String str) {
            return NumberUtils.createNumber(str);
        }

        private NumberFormat numberFormat() {
            if (numberFormat == null) {
                numberFormat = new DecimalFormat();
                numberFormat.setGroupingUsed(false);
            }
            return numberFormat;
        }

        public List filterOptions(List options) {
            if (options.isEmpty()) {
                return options;
            }
            if (normalize) {
                options = options.stream()
                    // qualify names and transform from Kind.OPTION to Kind.MAP
                    .map(o -> {
                        if (o.isParenthesized()) {
                            String resolved = resolve(this::getExtendFieldForFullName, o.getName(), true);
                            if (resolved != null) {
                                return transform(new OptionElement(
                                    resolved,
                                    o.getKind(),
                                    o.getValue(),
                                    o.isParenthesized())
                                );
                            }
                        }
                        return o;
                    })
                    .sorted(Comparator.comparing(OptionElement::getName))
                    .collect(Collectors.groupingBy(OptionElement::getName,
                        LinkedHashMap::new,  // deterministic order
                        Collectors.toList()))
                    .entrySet()
                    .stream()
                    // merge option maps for non-repeated options
                    .flatMap(entry -> {
                        String name = entry.getKey();
                        List list = entry.getValue();
                        ExtendFieldElementInfo fieldInfo = getExtendFieldForFullName(name, true);
                        if (fieldInfo != null && !fieldInfo.isRepeated() && list.size() > 0) {
                            return Stream.of(list.stream().reduce(ProtobufSchema::merge).get());
                        } else {
                            return list.stream();
                        }
                    })
                    .collect(Collectors.toList());
            }
            return options;
        }
    }
}