// // Copyright © 2014, David Tesler (https://github.com/protobufel) // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of the <organization> nor the // names of its contributors may be used to endorse or promote products // derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // package com.github.protobufel.grammar; import static com.github.protobufel.grammar.ExtensionRegistries.buildFullRegistryOf; import static com.github.protobufel.grammar.PrimitiveTypesUtil.getSimpleFieldValue; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import com.github.protobufel.grammar.Exceptions.DescriptorValidationRuntimeException; import com.google.protobuf.DescriptorProtos.DescriptorProto; import com.google.protobuf.DescriptorProtos.FieldDescriptorProto; import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Type; import com.google.protobuf.DescriptorProtos.FileDescriptorProto; import com.google.protobuf.DescriptorProtos.FileDescriptorProtoOrBuilder; import com.google.protobuf.DescriptorProtos.MethodDescriptorProto; import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.DescriptorValidationException; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.Descriptors.MethodDescriptor; import com.google.protobuf.Descriptors.ServiceDescriptor; import com.google.protobuf.ExtensionRegistry; /** * Helps in building canonical FileDescriptors. * * @author protobufel@gmail.com David Tesler */ //TODO make public when its functionality fully finalized and stable. final class FileDescriptors { private FileDescriptors() {} public static Builder newBuilder() { return new Builder(); } public static Builder of(final Builder builder) { return new Builder(builder); } final static class Builder { private boolean protocCompatible; private boolean customOptionsAsExtensions; private ExtensionRegistry registry; private FileDescriptor fileDescriptor; private FileDescriptorProto proto; private final List<FileDescriptor> dependencies; private boolean isBuilt; private Builder() { protocCompatible = false; customOptionsAsExtensions = true; registry = ExtensionRegistry.getEmptyRegistry(); fileDescriptor = null; proto = null; dependencies = new ArrayList<FileDescriptor>(); isBuilt = false; } private Builder(final boolean protocCompatible, final boolean customOptionsAsExtensions, final ExtensionRegistry registry, final FileDescriptor fileDescriptor, final FileDescriptorProto proto, final List<FileDescriptor> dependencies) { this.protocCompatible = protocCompatible; this.customOptionsAsExtensions = customOptionsAsExtensions; this.registry = Objects.requireNonNull(registry).getUnmodifiable(); this.fileDescriptor = fileDescriptor; this.proto = proto; this.dependencies = new ArrayList<FileDescriptor>(Objects.requireNonNull(dependencies)); isBuilt = false; } private Builder(final Builder other) { protocCompatible = other.protocCompatible; customOptionsAsExtensions = other.customOptionsAsExtensions; registry = other.registry; fileDescriptor = other.fileDescriptor; proto = other.proto; dependencies = new ArrayList<FileDescriptor>(other.dependencies); isBuilt = false; } protected boolean isBuilt() { return isBuilt; } public boolean isProtocCompatible() { return protocCompatible; } public Builder setProtocCompatible(final boolean protocCompatible) { if (this.protocCompatible != Objects.requireNonNull(protocCompatible)) { isBuilt = false; this.protocCompatible = protocCompatible; } return this; } public boolean isCustomOptionsAsExtensions() { return customOptionsAsExtensions; } public Builder setCustomOptionsAsExtensions(final boolean customOptionsAsExtensions) { if (this.customOptionsAsExtensions != Objects.requireNonNull(customOptionsAsExtensions)) { isBuilt = false; this.customOptionsAsExtensions = customOptionsAsExtensions; } return this; } public ExtensionRegistry getRegistry() { return registry; } public Builder clearRegistry() { return setRegistry(ExtensionRegistry.getEmptyRegistry()); } public Builder setRegistry(final ExtensionRegistry registry) { if (this.registry != Objects.requireNonNull(registry)) { isBuilt = false; this.registry = registry; } return this; } public FileDescriptor getFileDescriptor() { return fileDescriptor; } public Builder setFileDescriptor(final FileDescriptor fileDescriptor) { if (this.fileDescriptor != Objects.requireNonNull(fileDescriptor)) { isBuilt = false; this.fileDescriptor = fileDescriptor; proto = null; dependencies.clear(); } return this; } public FileDescriptorProto getProto() { return proto; } public Builder setProto(final FileDescriptorProtoOrBuilder proto) { if (this.proto != Objects.requireNonNull(proto)) { isBuilt = false; if (proto instanceof FileDescriptorProto.Builder) { this.proto = ((FileDescriptorProto.Builder) proto).build(); } else { this.proto = (FileDescriptorProto) proto; } fileDescriptor = null; } return this; } public List<FileDescriptor> getDependencies() { return Collections.unmodifiableList(dependencies); } public Builder clearDependencies() { if (!dependencies.isEmpty()) { isBuilt = false; dependencies.clear(); } return this; } public Builder addDependencies(final FileDescriptor... dependencies) { if (Objects.requireNonNull(dependencies).length == 0) { return this; } isBuilt = false; for (final FileDescriptor dependency : dependencies) { this.dependencies.add(Objects.requireNonNull(dependency)); } return this; } public FileDescriptor rebuild() { isBuilt = false; return build(); } public FileDescriptor build() { if (isBuilt) { return fileDescriptor; } if (fileDescriptor != null) { fileDescriptor = build(fileDescriptor); } else { Objects.requireNonNull(proto, "either proto or fileDescriptor must be set"); fileDescriptor = build(proto, dependencies.toArray(new FileDescriptor[0])); } if (registry != ExtensionRegistry.getEmptyRegistry()) { FileDescriptor.internalUpdateFileDescriptor(fileDescriptor, registry); } else if (customOptionsAsExtensions) { final ExtensionRegistry registry = buildFullRegistryOf(fileDescriptor); FileDescriptor.internalUpdateFileDescriptor(fileDescriptor, registry); } isBuilt = true; return fileDescriptor; } private FileDescriptor build(final FileDescriptor fileDescriptor) { try { return FileDescriptor.buildFrom(makeCanonicalProto(fileDescriptor), fileDescriptor .getDependencies().toArray(new FileDescriptor[0])); } catch (final DescriptorValidationException e) { throw new DescriptorValidationRuntimeException(e); } } private FileDescriptor build(final FileDescriptorProto proto, final FileDescriptor[] dependencies) { try { final FileDescriptor fileDescriptor = FileDescriptor.buildFrom(proto, dependencies); return FileDescriptor.buildFrom(makeCanonicalProto(fileDescriptor), dependencies); } catch (final DescriptorValidationException e) { throw new DescriptorValidationRuntimeException(e); } } private FileDescriptorProto makeCanonicalProto(final FileDescriptor fileDescriptor) { final FileDescriptorProto.Builder protoBuilder = FileDescriptorProto.newBuilder(fileDescriptor.toProto()); for (final FieldDescriptorProto.Builder field : protoBuilder.getExtensionBuilderList()) { makeCanonicalField(field, fileDescriptor.findExtensionByName(field.getName())); } for (final DescriptorProto.Builder message : protoBuilder.getMessageTypeBuilderList()) { makeCanonicalMessage(message, fileDescriptor.findMessageTypeByName(message.getName())); } // for (EnumDescriptorProto.Builder enumProto : // protoBuilder.getEnumTypeBuilderList()) { // makeCanonicalEnum(enumProto, // fileDescriptor.findEnumTypeByName(enumProto.getName())); // } for (final ServiceDescriptorProto.Builder serviceProto : protoBuilder.getServiceBuilderList()) { makeCanonicalService(serviceProto, fileDescriptor.findServiceByName(serviceProto.getName())); } // TODO: incorporate options' tree walking into canonicalization to eliminate double walking return OptionResolver.newBuilder().setCustomOptionsAsExtensions(false) .resolveAllOptionsFor(fileDescriptor, protoBuilder).build(); } private void makeCanonicalService(final ServiceDescriptorProto.Builder service, final ServiceDescriptor serviceDescriptor) { for (final MethodDescriptorProto.Builder method : service.getMethodBuilderList()) { final MethodDescriptor methodDescriptor = serviceDescriptor.findMethodByName(method.getName()); method.setInputType(ensureLeadingDot(methodDescriptor.getInputType().getFullName())); method.setOutputType(ensureLeadingDot(methodDescriptor.getOutputType().getFullName())); } } private void makeCanonicalMessage(final DescriptorProto.Builder message, final Descriptor messageDescriptor) { final List<FieldDescriptor> extensionsList = messageDescriptor.getExtensions(); if (extensionsList.size() != message.getExtensionBuilderList().size()) { throw new IllegalStateException(String.format( "Message %s has a size of extensions differ from its Descriptor!", message.getName())); } if (!extensionsList.isEmpty()) { final Map<Integer, FieldDescriptor> extensionMap = new HashMap<Integer, FieldDescriptor>(); for (final FieldDescriptor field : extensionsList) { extensionMap.put(field.getNumber(), field); } for (final FieldDescriptorProto.Builder field : message.getExtensionBuilderList()) { final FieldDescriptor extensionField = extensionMap.get(field.getNumber()); makeCanonicalField(field, extensionField); } } // for (FieldDescriptorProto.Builder field : // message.getExtensionBuilderList()) { // makeCanonicalField(field, // messageDescriptor.findFieldByNumber(field.getNumber())); // } for (final FieldDescriptorProto.Builder field : message.getFieldBuilderList()) { makeCanonicalField(field, messageDescriptor.findFieldByNumber(field.getNumber())); } for (final DescriptorProto.Builder child : message.getNestedTypeBuilderList()) { makeCanonicalMessage(child, messageDescriptor.findNestedTypeByName(child.getName())); } } private void makeCanonicalField(final FieldDescriptorProto.Builder field, final FieldDescriptor fieldDescriptor) { if (field.hasExtendee() && !field.getExtendee().startsWith(".")) { field.setExtendee(ensureLeadingDot(fieldDescriptor.getContainingType().getFullName())); } if (field.hasTypeName() && !field.getTypeName().startsWith(".")) { if (fieldDescriptor.getJavaType() == JavaType.ENUM) { field.setTypeName(ensureLeadingDot(fieldDescriptor.getEnumType().getFullName())); field.setType(Type.TYPE_ENUM); } else { // this must be a Message/Group field field.setTypeName(ensureLeadingDot(fieldDescriptor.getMessageType().getFullName())); if (!field.hasType()) { switch (fieldDescriptor.getType()) { case MESSAGE: field.setType(Type.TYPE_MESSAGE); break; case GROUP: field.setType(Type.TYPE_GROUP); break; default: field.setType(Type.valueOf("TYPE_" + fieldDescriptor.getType().name())); } } } } // reparsing of the default value if present - protoc does this contrary to the spec! if (protocCompatible) { if (field.hasDefaultValue()) { try { final String defaultValue = getSimpleFieldValue(fieldDescriptor, fieldDescriptor.getDefaultValue()); field.setDefaultValue(defaultValue); } catch (final IOException e) { // this should not happen! throw new RuntimeException(e); } } } } private String ensureLeadingDot(final String name) { return name.startsWith(".") ? name : "." + name; } } }