From 3bb7ca78f75dc1bc328375df27bfe31fd12784a7 Mon Sep 17 00:00:00 2001 From: Sandy Zhang Date: Fri, 20 Oct 2023 11:49:44 -0400 Subject: [PATCH] Breaking Change: Use Editions features in Java full runtimes. Note that this change breaks compatibility with old generated code from previous major versions per the Cross Version Runtime policy: https://protobuf.dev/support/cross-version-runtime-guarantee. This includes old gencode from <4.26.x, which does not resolve features. PiperOrigin-RevId: 575230560 --- conformance/BUILD.bazel | 2 + conformance/ConformanceJava.java | 132 ++--- conformance/failure_list_java.txt | 39 ++ conformance/text_format_failure_list_java.txt | 8 + java/core/BUILD.bazel | 34 ++ java/core/generate-sources-build.xml | 2 + java/core/pom.xml | 6 + .../java/com/google/protobuf/Descriptors.java | 479 ++++++++++++++++-- .../com/google/protobuf/GeneratedMessage.java | 2 +- .../JavaEditionDefaults.java.template | 9 + .../com/google/protobuf/DescriptorsTest.java | 22 + java/pom.xml | 1 + src/google/protobuf/compiler/java/file.cc | 109 +--- src/google/protobuf/compiler/java/file.h | 1 - .../protobuf/compiler/java/generator.cc | 3 +- .../compiler/java/shared_code_generator.cc | 12 + src/google/protobuf/editions/BUILD | 14 + 17 files changed, 672 insertions(+), 203 deletions(-) create mode 100644 java/core/src/main/java/com/google/protobuf/JavaEditionDefaults.java.template diff --git a/conformance/BUILD.bazel b/conformance/BUILD.bazel index 1d9ea1585c735..f0f50cae9c4de 100644 --- a/conformance/BUILD.bazel +++ b/conformance/BUILD.bazel @@ -230,6 +230,8 @@ java_binary( "//:protobuf_java_util", "//:test_messages_proto2_java_proto", "//:test_messages_proto3_java_proto", + "//src/google/protobuf/editions:test_messages_proto2_editions_java_proto", + "//src/google/protobuf/editions:test_messages_proto3_editions_java_proto", ], ) diff --git a/conformance/ConformanceJava.java b/conformance/ConformanceJava.java index e76dade5973d1..327dc7e8b8fba 100644 --- a/conformance/ConformanceJava.java +++ b/conformance/ConformanceJava.java @@ -15,6 +15,8 @@ import com.google.protobuf.conformance.Conformance; import com.google.protobuf.util.JsonFormat; import com.google.protobuf.util.JsonFormat.TypeRegistry; +import com.google.protobuf_test_messages.editions.proto2.TestMessagesProto2Editions; +import com.google.protobuf_test_messages.editions.proto3.TestMessagesProto3Editions; import com.google.protobuf_test_messages.proto2.TestMessagesProto2; import com.google.protobuf_test_messages.proto2.TestMessagesProto2.TestAllTypesProto2; import com.google.protobuf_test_messages.proto3.TestMessagesProto3; @@ -205,42 +207,61 @@ private T parseBinary( return messages.get(0); } + private Class createTestMessage(String messageType) { + if (messageType.equals("protobuf_test_messages.proto3.TestAllTypesProto3")) { + return TestAllTypesProto3.class; + } else if (messageType.equals("protobuf_test_messages.proto2.TestAllTypesProto2")) { + return TestAllTypesProto2.class; + } else if (messageType.equals("protobuf_test_messages.editions.proto3.TestAllTypesProto3")) { + return TestMessagesProto3Editions.TestAllTypesProto3.class; + } else if (messageType.equals("protobuf_test_messages.editions.proto2.TestAllTypesProto2")) { + return TestMessagesProto2Editions.TestAllTypesProto2.class; + } else { + throw new IllegalArgumentException( + "Protobuf request has unexpected payload type: " + messageType); + } + } + + private Class createTestFile(String messageType) { + if (messageType.equals("protobuf_test_messages.proto3.TestAllTypesProto3")) { + return TestMessagesProto3.class; + } else if (messageType.equals("protobuf_test_messages.proto2.TestAllTypesProto2")) { + return TestMessagesProto2.class; + } else if (messageType.equals("protobuf_test_messages.editions.proto3.TestAllTypesProto3")) { + return TestMessagesProto3Editions.class; + } else if (messageType.equals("protobuf_test_messages.editions.proto2.TestAllTypesProto2")) { + return TestMessagesProto2Editions.class; + } else { + throw new IllegalArgumentException( + "Protobuf request has unexpected payload type: " + messageType); + } + } + + @SuppressWarnings("unchecked") private Conformance.ConformanceResponse doTest(Conformance.ConformanceRequest request) { AbstractMessage testMessage; String messageType = request.getMessageType(); - boolean isProto3 = messageType.equals("protobuf_test_messages.proto3.TestAllTypesProto3"); - boolean isProto2 = messageType.equals("protobuf_test_messages.proto2.TestAllTypesProto2"); switch (request.getPayloadCase()) { case PROTOBUF_PAYLOAD: { - if (isProto3) { - try { - ExtensionRegistry extensions = ExtensionRegistry.newInstance(); - TestMessagesProto3.registerAllExtensions(extensions); - testMessage = - parseBinary( - request.getProtobufPayload(), TestAllTypesProto3.parser(), extensions); - } catch (InvalidProtocolBufferException e) { - return Conformance.ConformanceResponse.newBuilder() - .setParseError(e.getMessage()) - .build(); - } - } else if (isProto2) { - try { - ExtensionRegistry extensions = ExtensionRegistry.newInstance(); - TestMessagesProto2.registerAllExtensions(extensions); - testMessage = - parseBinary( - request.getProtobufPayload(), TestAllTypesProto2.parser(), extensions); - } catch (InvalidProtocolBufferException e) { - return Conformance.ConformanceResponse.newBuilder() - .setParseError(e.getMessage()) - .build(); - } - } else { - throw new IllegalArgumentException( - "Protobuf request has unexpected payload type: " + messageType); + try { + ExtensionRegistry extensions = ExtensionRegistry.newInstance(); + createTestFile(messageType) + .getMethod("registerAllExtensions", ExtensionRegistry.class) + .invoke(null, extensions); + testMessage = + parseBinary( + request.getProtobufPayload(), + (Parser) + createTestMessage(messageType).getMethod("parser").invoke(null), + extensions); + } catch (InvalidProtocolBufferException e) { + return Conformance.ConformanceResponse.newBuilder() + .setParseError(e.getMessage()) + .build(); + } catch (Exception e) { + throw new RuntimeException(e); } break; } @@ -252,54 +273,34 @@ private Conformance.ConformanceResponse doTest(Conformance.ConformanceRequest re == Conformance.TestCategory.JSON_IGNORE_UNKNOWN_PARSING_TEST) { parser = parser.ignoringUnknownFields(); } - if (isProto3) { - TestMessagesProto3.TestAllTypesProto3.Builder builder = - TestMessagesProto3.TestAllTypesProto3.newBuilder(); - parser.merge(request.getJsonPayload(), builder); - testMessage = builder.build(); - } else if (isProto2) { - TestMessagesProto2.TestAllTypesProto2.Builder builder = - TestMessagesProto2.TestAllTypesProto2.newBuilder(); - parser.merge(request.getJsonPayload(), builder); - testMessage = builder.build(); - } else { - throw new IllegalArgumentException( - "Protobuf request has unexpected payload type: " + messageType); - } + AbstractMessage.Builder builder = + (AbstractMessage.Builder) + createTestMessage(messageType).getMethod("newBuilder").invoke(null); + parser.merge(request.getJsonPayload(), builder); + testMessage = (AbstractMessage) builder.build(); } catch (InvalidProtocolBufferException e) { return Conformance.ConformanceResponse.newBuilder() .setParseError(e.getMessage()) .build(); + } catch (Exception e) { + throw new RuntimeException(e); } break; } case TEXT_PAYLOAD: { - if (isProto3) { - try { - TestMessagesProto3.TestAllTypesProto3.Builder builder = - TestMessagesProto3.TestAllTypesProto3.newBuilder(); - TextFormat.merge(request.getTextPayload(), builder); - testMessage = builder.build(); - } catch (TextFormat.ParseException e) { - return Conformance.ConformanceResponse.newBuilder() - .setParseError(e.getMessage()) - .build(); - } - } else if (isProto2) { - try { - TestMessagesProto2.TestAllTypesProto2.Builder builder = - TestMessagesProto2.TestAllTypesProto2.newBuilder(); - TextFormat.merge(request.getTextPayload(), builder); - testMessage = builder.build(); + try { + AbstractMessage.Builder builder = + (AbstractMessage.Builder) + createTestMessage(messageType).getMethod("newBuilder").invoke(null); + TextFormat.merge(request.getTextPayload(), builder); + testMessage = (AbstractMessage) builder.build(); } catch (TextFormat.ParseException e) { return Conformance.ConformanceResponse.newBuilder() .setParseError(e.getMessage()) .build(); - } - } else { - throw new IllegalArgumentException( - "Protobuf request has unexpected payload type: " + messageType); + } catch (Exception e) { + throw new RuntimeException(e); } break; } @@ -378,6 +379,9 @@ public void run() throws Exception { typeRegistry = TypeRegistry.newBuilder() .add(TestMessagesProto3.TestAllTypesProto3.getDescriptor()) + .add( + com.google.protobuf_test_messages.editions.proto3.TestMessagesProto3Editions + .TestAllTypesProto3.getDescriptor()) .build(); while (doTestIo()) { this.testCount++; diff --git a/conformance/failure_list_java.txt b/conformance/failure_list_java.txt index 8508abd31d965..7ee55ddacfb04 100644 --- a/conformance/failure_list_java.txt +++ b/conformance/failure_list_java.txt @@ -42,3 +42,42 @@ Required.Proto3.JsonInput.Int32FieldPlusSign Required.Proto3.JsonInput.RepeatedFieldWrongElementTypeExpectingStringsGotBool Required.Proto3.JsonInput.RepeatedFieldWrongElementTypeExpectingStringsGotInt Required.Proto3.JsonInput.StringFieldNotAString +Recommended.Editions_Proto3.FieldMaskNumbersDontRoundTrip.JsonOutput +Recommended.Editions_Proto3.FieldMaskPathsDontRoundTrip.JsonOutput +Recommended.Editions_Proto3.FieldMaskTooManyUnderscore.JsonOutput +Recommended.Editions_Proto3.JsonInput.BoolFieldAllCapitalFalse +Recommended.Editions_Proto3.JsonInput.BoolFieldAllCapitalTrue +Recommended.Editions_Proto3.JsonInput.BoolFieldCamelCaseFalse +Recommended.Editions_Proto3.JsonInput.BoolFieldCamelCaseTrue +Recommended.Editions_Proto3.JsonInput.BoolFieldDoubleQuotedFalse +Recommended.Editions_Proto3.JsonInput.BoolFieldDoubleQuotedTrue +Recommended.Editions_Proto3.JsonInput.BoolMapFieldKeyNotQuoted +Recommended.Editions_Proto3.JsonInput.DoubleFieldInfinityNotQuoted +Recommended.Editions_Proto3.JsonInput.DoubleFieldNanNotQuoted +Recommended.Editions_Proto3.JsonInput.DoubleFieldNegativeInfinityNotQuoted +Recommended.Editions_Proto3.JsonInput.FieldMaskInvalidCharacter +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicate +Recommended.Editions_Proto3.JsonInput.FieldNameNotQuoted +Recommended.Editions_Proto3.JsonInput.FloatFieldInfinityNotQuoted +Recommended.Editions_Proto3.JsonInput.FloatFieldNanNotQuoted +Recommended.Editions_Proto3.JsonInput.FloatFieldNegativeInfinityNotQuoted +Recommended.Editions_Proto3.JsonInput.Int32MapFieldKeyNotQuoted +Recommended.Editions_Proto3.JsonInput.Int64MapFieldKeyNotQuoted +Recommended.Editions_Proto3.JsonInput.JsonWithComments +Recommended.Editions_Proto3.JsonInput.StringFieldSingleQuoteBoth +Recommended.Editions_Proto3.JsonInput.StringFieldSingleQuoteKey +Recommended.Editions_Proto3.JsonInput.StringFieldSingleQuoteValue +Recommended.Editions_Proto3.JsonInput.StringFieldSurrogateInWrongOrder +Recommended.Editions_Proto3.JsonInput.StringFieldUnpairedHighSurrogate +Recommended.Editions_Proto3.JsonInput.StringFieldUnpairedLowSurrogate +Recommended.Editions_Proto3.JsonInput.Uint32MapFieldKeyNotQuoted +Recommended.Editions_Proto3.JsonInput.Uint64MapFieldKeyNotQuoted +Recommended.Editions_Proto2.JsonInput.FieldNameExtension.Validator +Required.Editions_Proto3.JsonInput.EnumFieldNotQuoted +Required.Editions_Proto3.JsonInput.Int32FieldLeadingZero +Required.Editions_Proto3.JsonInput.Int32FieldNegativeWithLeadingZero +Required.Editions_Proto3.JsonInput.Int32FieldPlusSign +Required.Editions_Proto3.JsonInput.RepeatedFieldWrongElementTypeExpectingStringsGotBool +Required.Editions_Proto3.JsonInput.RepeatedFieldWrongElementTypeExpectingStringsGotInt +Required.Editions_Proto3.JsonInput.StringFieldNotAString + diff --git a/conformance/text_format_failure_list_java.txt b/conformance/text_format_failure_list_java.txt index 793aae1281abe..8dea2862cdd4e 100644 --- a/conformance/text_format_failure_list_java.txt +++ b/conformance/text_format_failure_list_java.txt @@ -4,6 +4,14 @@ Recommended.Proto3.ProtobufInput.RepeatedUnknownFields_Drop.TextFormatOutput Recommended.Proto3.ProtobufInput.ScalarUnknownFields_Drop.TextFormatOutput Required.Proto3.TextFormatInput.AnyField.ProtobufOutput Required.Proto3.TextFormatInput.AnyField.TextFormatOutput +Recommended.Editions_Proto3.ProtobufInput.GroupUnknownFields_Drop.TextFormatOutput +Recommended.Editions_Proto3.ProtobufInput.MessageUnknownFields_Drop.TextFormatOutput +Recommended.Editions_Proto3.ProtobufInput.RepeatedUnknownFields_Drop.TextFormatOutput +Recommended.Editions_Proto3.ProtobufInput.ScalarUnknownFields_Drop.TextFormatOutput +Required.Editions_Proto3.TextFormatInput.AnyField.ProtobufOutput +Required.Editions_Proto3.TextFormatInput.AnyField.TextFormatOutput Required.Proto3.TextFormatInput.StringFieldBadUTF8Hex Required.Proto3.TextFormatInput.StringFieldBadUTF8Octal +Required.Editions_Proto3.TextFormatInput.StringFieldBadUTF8Hex +Required.Editions_Proto3.TextFormatInput.StringFieldBadUTF8Octal \ No newline at end of file diff --git a/java/core/BUILD.bazel b/java/core/BUILD.bazel index 4168a54b20eec..74fade146a24b 100644 --- a/java/core/BUILD.bazel +++ b/java/core/BUILD.bazel @@ -7,6 +7,7 @@ load("//:protobuf_version.bzl", "PROTOBUF_JAVA_VERSION") load("//build_defs:java_opts.bzl", "protobuf_java_export", "protobuf_java_library", "protobuf_versioned_java_library") load("//conformance:defs.bzl", "conformance_test") load("//java/internal:testing.bzl", "junit_tests") +load("//src/google/protobuf/editions:defaults.bzl", "compile_edition_defaults", "embed_edition_defaults") LITE_SRCS = [ # Keep in sync with `//java/lite:pom.xml`. @@ -168,13 +169,21 @@ protobuf_java_library( proto_library( name = "java_features_proto", srcs = ["src/main/java/com/google/protobuf/java_features.proto"], + strip_import_prefix = "/java/core/src/main/java/com", visibility = ["//pkg:__pkg__"], deps = ["//:descriptor_proto"], ) +filegroup( + name = "java_features_proto_srcs", + srcs = ["src/main/java/com/google/protobuf/java_features.proto"], + visibility = ["//pkg:__pkg__"], +) + internal_gen_well_known_protos_java( name = "gen_well_known_protos_java", deps = [ + ":java_features_proto", "//:any_proto", "//:api_proto", "//:compiler_plugin_proto", @@ -198,6 +207,7 @@ java_library( ], exclude = LITE_SRCS, ) + [ + "src/main/java/com/google/protobuf/JavaEditionDefaults.java", ":gen_well_known_protos_java", ], visibility = ["//visibility:public"], @@ -217,6 +227,7 @@ protobuf_versioned_java_library( ], exclude = LITE_SRCS, ) + [ + "src/main/java/com/google/protobuf/JavaEditionDefaults.java", ":gen_well_known_protos_java", ], automatic_module_name = "com.google.protobuf", @@ -240,6 +251,7 @@ protobuf_java_export( maven_coordinates = "com.google.protobuf:protobuf-java:%s" % PROTOBUF_JAVA_VERSION, pom_template = "pom_template.xml", resources = [ + ":java_features_proto_srcs", "//:well_known_type_protos", "//src/google/protobuf:descriptor_proto_srcs", ], @@ -266,6 +278,7 @@ proto_lang_toolchain( name = "toolchain", # keep this in sync w/ WELL_KNOWN_PROTO_MAP in //:BUILD blacklisted_protos = [ + ":java_features_proto", "//:any_proto", "//:api_proto", "//:compiler_plugin_proto", @@ -355,6 +368,7 @@ build_test( conformance_test( name = "conformance_test", failure_list = "//conformance:failure_list_java.txt", + maximum_edition = "2023", testee = "//conformance:conformance_java", text_format_failure_list = "//conformance:text_format_failure_list_java.txt", ) @@ -527,10 +541,29 @@ junit_tests( ], ) +compile_edition_defaults( + name = "java_edition_defaults", + srcs = [ + ":java_features_proto", + "//:descriptor_proto", + ], + maximum_edition = "2023", + minimum_edition = "PROTO2", +) + +embed_edition_defaults( + name = "embedded_java_edition_defaults_generate", + defaults = "java_edition_defaults", + output = "src/main/java/com/google/protobuf/JavaEditionDefaults.java", + placeholder = "DEFAULTS_VALUE", + template = "src/main/java/com/google/protobuf/JavaEditionDefaults.java.template", +) + pkg_files( name = "dist_files", srcs = glob([ "src/main/java/com/google/protobuf/*.java", + "src/main/java/com/google/protobuf/*.proto", "src/test/java/**/*.java", "src/test/proto/**/*.proto", ]) + [ @@ -539,6 +572,7 @@ pkg_files( "generate-test-sources-build.xml", "pom.xml", "pom_template.xml", + "src/main/java/com/google/protobuf/JavaEditionDefaults.java", ], strip_prefix = strip_prefix.from_root(""), visibility = ["//java:__pkg__"], diff --git a/java/core/generate-sources-build.xml b/java/core/generate-sources-build.xml index 0996e5fff43c9..4ffd4384dc416 100644 --- a/java/core/generate-sources-build.xml +++ b/java/core/generate-sources-build.xml @@ -4,6 +4,8 @@ + + diff --git a/java/core/pom.xml b/java/core/pom.xml index c98d168792f99..b0a2bab5cc490 100644 --- a/java/core/pom.xml +++ b/java/core/pom.xml @@ -59,6 +59,12 @@ google/protobuf/compiler/plugin.proto + + ${protobuf.java_source.dir} + + main/java/com/google/protobuf/java_features.proto + + diff --git a/java/core/src/main/java/com/google/protobuf/Descriptors.java b/java/core/src/main/java/com/google/protobuf/Descriptors.java index a8ed0e8d91556..f19caa390e24e 100644 --- a/java/core/src/main/java/com/google/protobuf/Descriptors.java +++ b/java/core/src/main/java/com/google/protobuf/Descriptors.java @@ -15,6 +15,9 @@ import com.google.protobuf.DescriptorProtos.EnumOptions; import com.google.protobuf.DescriptorProtos.EnumValueDescriptorProto; import com.google.protobuf.DescriptorProtos.EnumValueOptions; +import com.google.protobuf.DescriptorProtos.FeatureSet; +import com.google.protobuf.DescriptorProtos.FeatureSetDefaults; +import com.google.protobuf.DescriptorProtos.FeatureSetDefaults.FeatureSetEditionDefault; import com.google.protobuf.DescriptorProtos.FieldDescriptorProto; import com.google.protobuf.DescriptorProtos.FieldOptions; import com.google.protobuf.DescriptorProtos.FileDescriptorProto; @@ -27,6 +30,7 @@ import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto; import com.google.protobuf.DescriptorProtos.ServiceOptions; import com.google.protobuf.Descriptors.FileDescriptor.Syntax; +import com.google.protobuf.JavaFeaturesProto.JavaFeatures; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -65,6 +69,73 @@ public final class Descriptors { private static final EnumDescriptor[] EMPTY_ENUM_DESCRIPTORS = new EnumDescriptor[0]; private static final ServiceDescriptor[] EMPTY_SERVICE_DESCRIPTORS = new ServiceDescriptor[0]; private static final OneofDescriptor[] EMPTY_ONEOF_DESCRIPTORS = new OneofDescriptor[0]; + private static final HashMap FEATURE_CACHE = new HashMap<>(); + + @SuppressWarnings("ConstantField") + private static FeatureSetDefaults JAVA_EDITION_DEFAULTS = null; + + private static FeatureSet getEditionDefaults(Edition edition) { + // Force explicit initialization before synchronized block which can trigger initialization in + // `JavaFeaturesProto.registerAllExtensions()` and `FeatureSetdefaults.parseFrom()` calls. + // Otherwise, this can result in deadlock if another threads holds the static init block's + // implicit lock. This operation should be cheap if initialization has already occurred. + Descriptor unused1 = FeatureSetDefaults.getDescriptor(); + FileDescriptor unused2 = JavaFeaturesProto.getDescriptor(); + synchronized (Descriptors.class) { + if (JAVA_EDITION_DEFAULTS == null) { + try { + ExtensionRegistry registry = ExtensionRegistry.newInstance(); + JavaFeaturesProto.registerAllExtensions(registry); + JAVA_EDITION_DEFAULTS = + FeatureSetDefaults.parseFrom( + JavaEditionDefaults.PROTOBUF_INTERNAL_JAVA_EDITION_DEFAULTS.getBytes( + Internal.ISO_8859_1), + registry); + } catch (Exception e) { + throw new AssertionError(e); + } + } + } + + if (edition.getNumber() < JAVA_EDITION_DEFAULTS.getMinimumEdition().getNumber()) { + throw new IllegalArgumentException( + "Edition " + + edition + + " is lower than the minimum supported edition " + + JAVA_EDITION_DEFAULTS.getMinimumEdition() + + "!"); + } + if (edition.getNumber() > JAVA_EDITION_DEFAULTS.getMaximumEdition().getNumber()) { + throw new IllegalArgumentException( + "Edition " + + edition + + " is greater than the maximum supported edition " + + JAVA_EDITION_DEFAULTS.getMaximumEdition() + + "!"); + } + FeatureSet found = null; + for (FeatureSetEditionDefault editionDefault : JAVA_EDITION_DEFAULTS.getDefaultsList()) { + if (editionDefault.getEdition().getNumber() > edition.getNumber()) { + break; + } + found = editionDefault.getFeatures(); + } + if (found == null) { + throw new IllegalArgumentException( + "Edition " + edition + " does not have a valid default FeatureSet!"); + } + return found; + } + + private static synchronized FeatureSet internFeatures(FeatureSet features) { + ByteString serialized = features.toByteString(); + FeatureSet cached = FEATURE_CACHE.get(serialized); + if (cached == null) { + FEATURE_CACHE.put(serialized, features); + cached = features; + } + return cached; + } /** * Describes a {@code .proto} file, including everything defined within. That includes, in @@ -106,7 +177,15 @@ public String getPackage() { /** Get the {@code FileOptions}, defined in {@code descriptor.proto}. */ public FileOptions getOptions() { - return proto.getOptions(); + synchronized (this) { + if (this.options == null) { + this.options = proto.getOptions(); + if (this.options.hasFeatures()) { + this.options = this.options.toBuilder().clearFeatures().build(); + } + } + } + return this.options; } /** Get a list of top-level message types declared in this file. */ @@ -165,6 +244,11 @@ Syntax getSyntax() { /** Get the edition of the .proto file. */ public Edition getEdition() { + if (getSyntax().equals(Syntax.PROTO2)) { + return Edition.EDITION_PROTO2; + } else if (getSyntax().equals(Syntax.PROTO3)) { + return Edition.EDITION_PROTO3; + } return proto.getEdition(); } @@ -314,6 +398,15 @@ public static FileDescriptor buildFrom(FileDescriptorProto proto, FileDescriptor public static FileDescriptor buildFrom( FileDescriptorProto proto, FileDescriptor[] dependencies, boolean allowUnknownDependencies) throws DescriptorValidationException { + return buildFrom(proto, dependencies, allowUnknownDependencies, false); + } + + private static FileDescriptor buildFrom( + FileDescriptorProto proto, + FileDescriptor[] dependencies, + boolean allowUnknownDependencies, + boolean allowUnresolvedFeatures) + throws DescriptorValidationException { // Building descriptors involves two steps: translating and linking. // In the translation step (implemented by FileDescriptor's // constructor), we build an object tree mirroring the @@ -327,6 +420,10 @@ public static FileDescriptor buildFrom( FileDescriptor result = new FileDescriptor(proto, dependencies, pool, allowUnknownDependencies); result.crossLink(); + // Skip feature resolution until later for calls from gencode. + if (!allowUnresolvedFeatures) { + result.resolveAllFeatures(); + } return result; } @@ -430,8 +527,8 @@ public static FileDescriptor internalBuildGeneratedFileFrom( try { // When building descriptors for generated code, we allow unknown - // dependencies by default. - return buildFrom(proto, dependencies, true); + // dependencies by default and delay feature resolution until later. + return buildFrom(proto, dependencies, true, true); } catch (DescriptorValidationException e) { throw new IllegalArgumentException( "Invalid embedded descriptor for \"" + proto.getName() + "\".", e); @@ -504,6 +601,7 @@ public interface InternalDescriptorAssigner { } private FileDescriptorProto proto; + private FileOptions options; private final Descriptor[] messageTypes; private final EnumDescriptor[] enumTypes; private final ServiceDescriptor[] services; @@ -582,6 +680,7 @@ private FileDescriptor( /** Create a placeholder FileDescriptor for a message Descriptor. */ FileDescriptor(String packageName, Descriptor message) throws DescriptorValidationException { + this.parent = null; this.pool = new DescriptorPool(new FileDescriptor[0], true); this.proto = FileDescriptorProto.newBuilder() @@ -604,11 +703,61 @@ private FileDescriptor( /** * This method is to be called by generated code only. It resolves features for the descriptor * and all of its children. - * - *

TODO Implement and use this method in gencode once users using prebuilt jars - * with stale runtimes updated. */ - public void resolveAllFeatures() {} + public synchronized void resolveAllFeatures() { + if (this.features != null) { + return; + } + this.features = resolveFeatures(proto.getOptions().getFeatures()); + + for (final Descriptor messageType : messageTypes) { + messageType.resolveAllFeatures(); + } + + for (final EnumDescriptor enumType : enumTypes) { + enumType.resolveAllFeatures(); + } + + for (final ServiceDescriptor service : services) { + service.resolveAllFeatures(); + } + + for (final FieldDescriptor extension : extensions) { + extension.resolveAllFeatures(); + } + } + + @Override + FeatureSet inferLegacyProtoFeatures() { + FeatureSet.Builder features = FeatureSet.newBuilder(); + if (getEdition().getNumber() >= Edition.EDITION_2023.getNumber()) { + return features.build(); + } + + if (getEdition() == Edition.EDITION_PROTO2) { + if (proto.getOptions().getJavaStringCheckUtf8()) { + features.setExtension( + JavaFeaturesProto.java, + JavaFeatures.newBuilder() + .setUtf8Validation(JavaFeatures.Utf8Validation.VERIFY) + .build()); + } + } + return features.build(); + } + + @Override + boolean hasInferredLegacyProtoFeatures() { + if (getEdition().getNumber() >= Edition.EDITION_2023.getNumber()) { + return false; + } + if (getEdition() == Edition.EDITION_PROTO2) { + if (proto.getOptions().getJavaStringCheckUtf8()) { + return true; + } + } + return false; + } /** Look up and cross-link all field types, etc. */ private void crossLink() throws DescriptorValidationException { @@ -635,6 +784,9 @@ private void crossLink() throws DescriptorValidationException { */ private void setProto(final FileDescriptorProto proto) { this.proto = proto; + this.features = null; + this.options = null; + this.features = resolveFeatures(proto.getOptions().getFeatures()); for (int i = 0; i < messageTypes.length; i++) { messageTypes[i].setProto(proto.getMessageType(i)); @@ -720,7 +872,15 @@ public Descriptor getContainingType() { /** Get the {@code MessageOptions}, defined in {@code descriptor.proto}. */ public MessageOptions getOptions() { - return proto.getOptions(); + synchronized (this) { + if (this.options == null) { + this.options = proto.getOptions(); + if (this.options.hasFeatures()) { + this.options = this.options.toBuilder().clearFeatures().build(); + } + } + } + return this.options; } /** Get a list of this message type's fields. */ @@ -855,6 +1015,7 @@ public EnumDescriptor findEnumTypeByName(final String name) { private final int index; private DescriptorProto proto; + private MessageOptions options; private final String fullName; private final FileDescriptor file; private final Descriptor containingType; @@ -898,6 +1059,7 @@ public EnumDescriptor findEnumTypeByName(final String name) { // Create a placeholder FileDescriptor to hold this message. this.file = new FileDescriptor(packageName, this); + this.parent = this.file; extensionRangeLowerBounds = new int[] {1}; extensionRangeUpperBounds = new int[] {536870912}; @@ -909,6 +1071,11 @@ private Descriptor( final Descriptor parent, final int index) throws DescriptorValidationException { + if (parent == null) { + this.parent = file; + } else { + this.parent = parent; + } this.index = index; this.proto = proto; fullName = computeFullName(file, parent, proto.getName()); @@ -1002,6 +1169,31 @@ private Descriptor( } } + /** See {@link FileDescriptor#resolveAllFeatures}. */ + private void resolveAllFeatures() { + this.features = resolveFeatures(proto.getOptions().getFeatures()); + + for (final Descriptor nestedType : nestedTypes) { + nestedType.resolveAllFeatures(); + } + + for (final EnumDescriptor enumType : enumTypes) { + enumType.resolveAllFeatures(); + } + + for (final FieldDescriptor field : fields) { + field.resolveAllFeatures(); + } + + for (final FieldDescriptor extension : extensions) { + extension.resolveAllFeatures(); + } + + for (final OneofDescriptor oneof : oneofs) { + oneof.resolveAllFeatures(); + } + } + /** Look up and cross-link all field types, etc. */ private void crossLink() throws DescriptorValidationException { for (final Descriptor nestedType : nestedTypes) { @@ -1040,6 +1232,9 @@ private void validateNoDuplicateFieldNumbers() throws DescriptorValidationExcept /** See {@link FileDescriptor#setProto}. */ private void setProto(final DescriptorProto proto) { this.proto = proto; + this.features = null; + this.options = null; + this.features = resolveFeatures(proto.getOptions().getFeatures()); for (int i = 0; i < nestedTypes.length; i++) { nestedTypes[i].setProto(proto.getNestedType(i)); @@ -1147,6 +1342,14 @@ public FileDescriptor getFile() { /** Get the field's declared type. */ public Type getType() { + // Override delimited messages as legacy group type. Leaves unresolved messages as-is + // since these are used before feature resolution when parsing java feature set defaults + // (custom options) into unknown fields. + if (type == Type.MESSAGE + && this.features != null + && this.features.getMessageEncoding() == FeatureSet.MessageEncoding.DELIMITED) { + return Type.GROUP; + } return type; } @@ -1165,10 +1368,13 @@ public boolean needsUtf8Check() { // Always enforce strict UTF-8 checking for map fields. return true; } - if (getFile().getSyntax() == Syntax.PROTO3) { + if (this.features + .getExtension(JavaFeaturesProto.java) + .getUtf8Validation() + .equals(JavaFeatures.Utf8Validation.VERIFY)) { return true; } - return getFile().getOptions().getJavaStringCheckUtf8(); + return this.features.getUtf8Validation().equals(FeatureSet.Utf8Validation.VERIFY); } public boolean isMapField() { @@ -1184,7 +1390,8 @@ && isRepeated() /** Is this field declared required? */ public boolean isRequired() { - return proto.getLabel() == FieldDescriptorProto.Label.LABEL_REQUIRED; + return this.features.getFieldPresence() + == DescriptorProtos.FeatureSet.FieldPresence.LEGACY_REQUIRED; } /** Is this field declared optional? */ @@ -1207,11 +1414,9 @@ public boolean isPacked() { if (!isPackable()) { return false; } - if (getFile().getSyntax() == FileDescriptor.Syntax.PROTO2) { - return getOptions().getPacked(); - } else { - return !getOptions().hasPacked() || getOptions().getPacked(); - } + return this.features + .getRepeatedFieldEncoding() + .equals(FeatureSet.RepeatedFieldEncoding.PACKED); } /** Can this field be packed? That is, is it a repeated primitive field? */ @@ -1239,7 +1444,15 @@ public Object getDefaultValue() { /** Get the {@code FieldOptions}, defined in {@code descriptor.proto}. */ public FieldOptions getOptions() { - return proto.getOptions(); + synchronized (this) { + if (this.options == null) { + this.options = proto.getOptions(); + if (this.options.hasFeatures()) { + this.options = this.options.toBuilder().clearFeatures().build(); + } + } + } + return this.options; } /** Is this field an extension? */ @@ -1271,7 +1484,9 @@ public OneofDescriptor getRealContainingOneof() { */ boolean hasOptionalKeyword() { return isProto3Optional - || (file.getSyntax() == Syntax.PROTO2 && isOptional() && getContainingOneof() == null); + || (file.getEdition() == Edition.EDITION_PROTO2 + && isOptional() + && getContainingOneof() == null); } /** @@ -1291,7 +1506,7 @@ public boolean hasPresence() { return getType() == Type.MESSAGE || getType() == Type.GROUP || getContainingOneof() != null - || file.getSyntax() == Syntax.PROTO2; + || this.features.getFieldPresence() != DescriptorProtos.FeatureSet.FieldPresence.IMPLICIT; } /** @@ -1363,7 +1578,13 @@ public EnumDescriptor getEnumType() { * handling quirks. */ public boolean legacyEnumFieldTreatedAsClosed() { - return getType() == Type.ENUM && getFile().getSyntax() == Syntax.PROTO2; + // TODO: Remove? + if (getFile().getFullName().equals("google/protobuf/descriptor.proto")) { + return true; + } + return getType() == Type.ENUM + && (this.features.getExtension(JavaFeaturesProto.java).getLegacyClosedEnum() + || enumType.isClosed()); } /** @@ -1392,6 +1613,7 @@ public String toString() { private final int index; private FieldDescriptorProto proto; + private FieldOptions options; private final String fullName; private String jsonName; private final FileDescriptor file; @@ -1510,6 +1732,7 @@ private FieldDescriptor( final int index, final boolean isExtension) throws DescriptorValidationException { + this.parent = parent; this.index = index; this.proto = proto; fullName = computeFullName(file, parent, proto.getName()); @@ -1567,6 +1790,69 @@ private FieldDescriptor( file.pool.addSymbol(this); } + /** See {@link FileDescriptor#resolveAllFeatures}. */ + private void resolveAllFeatures() { + this.features = resolveFeatures(proto.getOptions().getFeatures()); + } + + @Override + FeatureSet inferLegacyProtoFeatures() { + FeatureSet.Builder features = FeatureSet.newBuilder(); + if (getFile().getEdition().getNumber() >= Edition.EDITION_2023.getNumber()) { + return features.build(); + } + + if (proto.getLabel() == FieldDescriptorProto.Label.LABEL_REQUIRED) { + features.setFieldPresence(FeatureSet.FieldPresence.LEGACY_REQUIRED); + } + + if (proto.getType() == FieldDescriptorProto.Type.TYPE_GROUP) { + features.setMessageEncoding(FeatureSet.MessageEncoding.DELIMITED); + } + + if (proto.getOptions().getPacked()) { + features.setRepeatedFieldEncoding(FeatureSet.RepeatedFieldEncoding.PACKED); + } + + if (getFile().getEdition() == Edition.EDITION_PROTO3) { + if (proto.getOptions().hasPacked() && !proto.getOptions().getPacked()) { + features.setRepeatedFieldEncoding(FeatureSet.RepeatedFieldEncoding.EXPANDED); + } + + } + return features.build(); + } + + @Override + boolean hasInferredLegacyProtoFeatures() { + if (getFile().getEdition().getNumber() >= Edition.EDITION_2023.getNumber()) { + return false; + } + + if (proto.getLabel() == FieldDescriptorProto.Label.LABEL_REQUIRED) { + return true; + } + + if (proto.getType() == FieldDescriptorProto.Type.TYPE_GROUP) { + return true; + } + + if (proto.getOptions().getPacked()) { + return true; + } + + if (getFile().getEdition() == Edition.EDITION_PROTO3) { + if (proto.getOptions().hasPacked() && !proto.getOptions().getPacked()) { + return true; + } + if (isProto3Optional) { + return true; + } + + } + return false; + } + /** Look up and cross-link all field types, etc. */ private void crossLink() throws DescriptorValidationException { if (proto.hasExtendee()) { @@ -1755,6 +2041,9 @@ private void crossLink() throws DescriptorValidationException { /** See {@link FileDescriptor#setProto}. */ private void setProto(final FieldDescriptorProto proto) { this.proto = proto; + this.features = null; + this.options = null; + this.features = resolveFeatures(proto.getOptions().getFeatures()); } /** For internal use only. This is to satisfy the FieldDescriptorLite interface. */ @@ -1833,7 +2122,7 @@ public FileDescriptor getFile() { * handling quirks. */ public boolean isClosed() { - return getFile().getSyntax() != Syntax.PROTO3; + return this.features.getEnumType() == DescriptorProtos.FeatureSet.EnumType.CLOSED; } /** If this is a nested type, get the outer descriptor, otherwise null. */ @@ -1843,7 +2132,15 @@ public Descriptor getContainingType() { /** Get the {@code EnumOptions}, defined in {@code descriptor.proto}. */ public EnumOptions getOptions() { - return proto.getOptions(); + synchronized (this) { + if (this.options == null) { + this.options = proto.getOptions(); + if (this.options.hasFeatures()) { + this.options = this.options.toBuilder().clearFeatures().build(); + } + } + } + return this.options; } /** Get a list of defined values for this enum. */ @@ -1954,6 +2251,7 @@ int getUnknownEnumValueDescriptorCount() { private final int index; private EnumDescriptorProto proto; + private EnumOptions options; private final String fullName; private final FileDescriptor file; private final Descriptor containingType; @@ -1969,6 +2267,11 @@ private EnumDescriptor( final Descriptor parent, final int index) throws DescriptorValidationException { + if (parent == null) { + this.parent = file; + } else { + this.parent = parent; + } this.index = index; this.proto = proto; fullName = computeFullName(file, parent, proto.getName()); @@ -2002,9 +2305,20 @@ private EnumDescriptor( file.pool.addSymbol(this); } + /** See {@link FileDescriptor#resolveAllFeatures}. */ + private void resolveAllFeatures() { + this.features = resolveFeatures(proto.getOptions().getFeatures()); + for (EnumValueDescriptor value : values) { + value.resolveAllFeatures(); + } + } + /** See {@link FileDescriptor#setProto}. */ private void setProto(final EnumDescriptorProto proto) { this.proto = proto; + this.features = null; + this.options = null; + this.features = resolveFeatures(proto.getOptions().getFeatures()); for (int i = 0; i < values.length; i++) { values[i].setProto(proto.getValue(i)); @@ -2092,11 +2406,20 @@ public EnumDescriptor getType() { /** Get the {@code EnumValueOptions}, defined in {@code descriptor.proto}. */ public EnumValueOptions getOptions() { - return proto.getOptions(); + synchronized (this) { + if (this.options == null) { + this.options = proto.getOptions(); + if (this.options.hasFeatures()) { + this.options = this.options.toBuilder().clearFeatures().build(); + } + } + } + return this.options; } private final int index; private EnumValueDescriptorProto proto; + private EnumValueOptions options; private final String fullName; private final EnumDescriptor type; @@ -2106,12 +2429,11 @@ private EnumValueDescriptor( final EnumDescriptor parent, final int index) throws DescriptorValidationException { + this.parent = parent; this.index = index; this.proto = proto; - type = parent; - - fullName = parent.getFullName() + '.' + proto.getName(); - + this.type = parent; + this.fullName = parent.getFullName() + '.' + proto.getName(); file.pool.addSymbol(this); } @@ -2120,6 +2442,7 @@ private EnumValueDescriptor(final EnumDescriptor parent, final Integer number) { String name = "UNKNOWN_ENUM_VALUE_" + parent.getName() + "_" + number; EnumValueDescriptorProto proto = EnumValueDescriptorProto.newBuilder().setName(name).setNumber(number).build(); + this.parent = parent; this.index = -1; this.proto = proto; this.type = parent; @@ -2128,9 +2451,17 @@ private EnumValueDescriptor(final EnumDescriptor parent, final Integer number) { // Don't add this descriptor into pool. } + /** See {@link FileDescriptor#resolveAllFeatures}. */ + private void resolveAllFeatures() { + this.features = resolveFeatures(proto.getOptions().getFeatures()); + } + /** See {@link FileDescriptor#setProto}. */ private void setProto(final EnumValueDescriptorProto proto) { this.proto = proto; + this.features = null; + this.options = null; + this.features = resolveFeatures(proto.getOptions().getFeatures()); } } @@ -2175,7 +2506,15 @@ public FileDescriptor getFile() { /** Get the {@code ServiceOptions}, defined in {@code descriptor.proto}. */ public ServiceOptions getOptions() { - return proto.getOptions(); + synchronized (this) { + if (this.options == null) { + this.options = proto.getOptions(); + if (this.options.hasFeatures()) { + this.options = this.options.toBuilder().clearFeatures().build(); + } + } + } + return this.options; } /** Get a list of methods for this service. */ @@ -2200,6 +2539,7 @@ public MethodDescriptor findMethodByName(final String name) { private final int index; private ServiceDescriptorProto proto; + private ServiceOptions options; private final String fullName; private final FileDescriptor file; private MethodDescriptor[] methods; @@ -2207,6 +2547,7 @@ public MethodDescriptor findMethodByName(final String name) { private ServiceDescriptor( final ServiceDescriptorProto proto, final FileDescriptor file, final int index) throws DescriptorValidationException { + this.parent = file; this.index = index; this.proto = proto; fullName = computeFullName(file, null, proto.getName()); @@ -2220,6 +2561,15 @@ private ServiceDescriptor( file.pool.addSymbol(this); } + /** See {@link FileDescriptor#resolveAllFeatures}. */ + private void resolveAllFeatures() { + this.features = resolveFeatures(proto.getOptions().getFeatures()); + + for (final MethodDescriptor method : methods) { + method.resolveAllFeatures(); + } + } + private void crossLink() throws DescriptorValidationException { for (final MethodDescriptor method : methods) { method.crossLink(); @@ -2229,6 +2579,9 @@ private void crossLink() throws DescriptorValidationException { /** See {@link FileDescriptor#setProto}. */ private void setProto(final ServiceDescriptorProto proto) { this.proto = proto; + this.features = null; + this.options = null; + this.features = resolveFeatures(proto.getOptions().getFeatures()); for (int i = 0; i < methods.length; i++) { methods[i].setProto(proto.getMethod(i)); @@ -2302,11 +2655,20 @@ public boolean isServerStreaming() { /** Get the {@code MethodOptions}, defined in {@code descriptor.proto}. */ public MethodOptions getOptions() { - return proto.getOptions(); + synchronized (this) { + if (this.options == null) { + this.options = proto.getOptions(); + if (this.options.hasFeatures()) { + this.options = this.options.toBuilder().clearFeatures().build(); + } + } + } + return this.options; } private final int index; private MethodDescriptorProto proto; + private MethodOptions options; private final String fullName; private final FileDescriptor file; private final ServiceDescriptor service; @@ -2321,6 +2683,7 @@ private MethodDescriptor( final ServiceDescriptor parent, final int index) throws DescriptorValidationException { + this.parent = parent; this.index = index; this.proto = proto; this.file = file; @@ -2331,6 +2694,11 @@ private MethodDescriptor( file.pool.addSymbol(this); } + /** See {@link FileDescriptor#resolveAllFeatures}. */ + private void resolveAllFeatures() { + this.features = resolveFeatures(proto.getOptions().getFeatures()); + } + private void crossLink() throws DescriptorValidationException { final GenericDescriptor input = getFile() @@ -2356,6 +2724,9 @@ private void crossLink() throws DescriptorValidationException { /** See {@link FileDescriptor#setProto}. */ private void setProto(final MethodDescriptorProto proto) { this.proto = proto; + this.features = null; + this.options = null; + this.features = resolveFeatures(proto.getOptions().getFeatures()); } } @@ -2382,7 +2753,6 @@ private static String computeFullName( * DescriptorPool}. */ public abstract static class GenericDescriptor { - // Private constructor to prevent subclasses outside of com.google.protobuf.Descriptors private GenericDescriptor() {} @@ -2393,6 +2763,35 @@ private GenericDescriptor() {} public abstract String getFullName(); public abstract FileDescriptor getFile(); + + FeatureSet resolveFeatures(FeatureSet unresolvedFeatures) throws RuntimeException { + if (this.parent != null + && unresolvedFeatures.equals(FeatureSet.getDefaultInstance()) + && !hasInferredLegacyProtoFeatures()) { + return this.parent.features; + } + FeatureSet.Builder features; + if (this.parent == null) { + Edition edition = getFile().getEdition(); + features = getEditionDefaults(edition).toBuilder(); + } else { + features = this.parent.features.toBuilder(); + } + features.mergeFrom(inferLegacyProtoFeatures()); + features.mergeFrom(unresolvedFeatures); + return internFeatures(features.build()); + } + + FeatureSet inferLegacyProtoFeatures() { + return FeatureSet.getDefaultInstance(); + } + + boolean hasInferredLegacyProtoFeatures() { + return false; + } + + GenericDescriptor parent; + FeatureSet features; } /** Thrown when building descriptors fails because the source DescriptorProtos are not valid. */ @@ -2820,7 +3219,15 @@ public int getFieldCount() { } public OneofOptions getOptions() { - return proto.getOptions(); + synchronized (this) { + if (this.options == null) { + this.options = proto.getOptions(); + if (this.options.hasFeatures()) { + this.options = this.options.toBuilder().clearFeatures().build(); + } + } + } + return this.options; } /** Get a list of this message type's fields. */ @@ -2841,8 +3248,16 @@ boolean isSynthetic() { return fields.length == 1 && fields[0].isProto3Optional; } + /** See {@link FileDescriptor#resolveAllFeatures}. */ + private void resolveAllFeatures() { + this.features = resolveFeatures(proto.getOptions().getFeatures()); + } + private void setProto(final OneofDescriptorProto proto) { this.proto = proto; + this.features = null; + this.options = null; + this.features = resolveFeatures(proto.getOptions().getFeatures()); } private OneofDescriptor( @@ -2850,6 +3265,7 @@ private OneofDescriptor( final FileDescriptor file, final Descriptor parent, final int index) { + this.parent = parent; this.proto = proto; fullName = computeFullName(file, parent, proto.getName()); this.file = file; @@ -2861,6 +3277,7 @@ private OneofDescriptor( private final int index; private OneofDescriptorProto proto; + private OneofOptions options; private final String fullName; private final FileDescriptor file; diff --git a/java/core/src/main/java/com/google/protobuf/GeneratedMessage.java b/java/core/src/main/java/com/google/protobuf/GeneratedMessage.java index d3ff341dc008d..f722f8852c043 100644 --- a/java/core/src/main/java/com/google/protobuf/GeneratedMessage.java +++ b/java/core/src/main/java/com/google/protobuf/GeneratedMessage.java @@ -1613,7 +1613,7 @@ GeneratedExtension newFileScopedGeneratedExtension( protected FieldDescriptor loadDescriptor() { try { Class clazz = singularType.getClassLoader().loadClass(descriptorOuterClass); - FileDescriptor file = (FileDescriptor) clazz.getField("descriptor").get(null); + FileDescriptor file = (FileDescriptor) clazz.getMethod("getDescriptor").invoke(null); return file.findExtensionByName(extensionName); } catch (Exception e) { throw new RuntimeException( diff --git a/java/core/src/main/java/com/google/protobuf/JavaEditionDefaults.java.template b/java/core/src/main/java/com/google/protobuf/JavaEditionDefaults.java.template new file mode 100644 index 0000000000000..bd71f453ef3cd --- /dev/null +++ b/java/core/src/main/java/com/google/protobuf/JavaEditionDefaults.java.template @@ -0,0 +1,9 @@ +// This file contains the serialized FeatureSetDefaults object corresponding to +// the Java runtime. This is used for feature resolution under Editions. + +package com.google.protobuf; + +public final class JavaEditionDefaults { + public static final String PROTOBUF_INTERNAL_JAVA_EDITION_DEFAULTS = "DEFAULTS_VALUE"; +} + diff --git a/java/core/src/test/java/com/google/protobuf/DescriptorsTest.java b/java/core/src/test/java/com/google/protobuf/DescriptorsTest.java index aae4a4dd13889..8cb07a13cdf8e 100644 --- a/java/core/src/test/java/com/google/protobuf/DescriptorsTest.java +++ b/java/core/src/test/java/com/google/protobuf/DescriptorsTest.java @@ -9,6 +9,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; import com.google.protobuf.DescriptorProtos.DescriptorProto; import com.google.protobuf.DescriptorProtos.Edition; @@ -135,6 +136,27 @@ public void testFileDescriptorGetSyntax() throws Exception { FileDescriptorProto proto3 = FileDescriptorProto.newBuilder().setSyntax("proto3").build(); FileDescriptor file3 = Descriptors.FileDescriptor.buildFrom(proto3, new FileDescriptor[0]); assertThat(file3.getSyntax()).isEqualTo(Descriptors.FileDescriptor.Syntax.PROTO3); + + FileDescriptorProto protoEdition = + FileDescriptorProto.newBuilder() + .setSyntax("editions") + .setEdition(Edition.EDITION_2023) + .build(); + FileDescriptor fileEdition = + Descriptors.FileDescriptor.buildFrom(protoEdition, new FileDescriptor[0]); + assertThat(fileEdition.getSyntax()).isEqualTo(Descriptors.FileDescriptor.Syntax.EDITIONS); + assertThat(fileEdition.getEdition()).isEqualTo(Edition.EDITION_2023); + assertThat(fileEdition.getEditionName()).isEqualTo("2023"); + + FileDescriptorProto protoMissingEdition = + FileDescriptorProto.newBuilder().setSyntax("editions").build(); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> Descriptors.FileDescriptor.buildFrom(protoMissingEdition, new FileDescriptor[0])); + assertThat(exception) + .hasMessageThat() + .contains("Edition EDITION_UNKNOWN is lower than the minimum supported edition"); } @Test diff --git a/java/pom.xml b/java/pom.xml index 9e0a6992c01fa..89b155d989a00 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -33,6 +33,7 @@ ${project.basedir}/../.. ${protobuf.basedir}/src + ${protobuf.basedir}/java/core/src ${protobuf.basedir}/protoc src/test/proto ${project.build.directory}/generated-sources diff --git a/src/google/protobuf/compiler/java/file.cc b/src/google/protobuf/compiler/java/file.cc index 0afaa47f71d75..e051f295888c4 100644 --- a/src/google/protobuf/compiler/java/file.cc +++ b/src/google/protobuf/compiler/java/file.cc @@ -359,8 +359,6 @@ void FileGenerator::Generate(io::Printer* printer) { if (HasDescriptorMethods(file_, context_->EnforceLite())) { if (immutable_api_) { GenerateDescriptorInitializationCodeForImmutable(printer); - } else { - GenerateDescriptorInitializationCodeForMutable(printer); } } else { printer->Print("static {\n"); @@ -428,6 +426,10 @@ void FileGenerator::GenerateDescriptorInitializationCodeForImmutable( "_clinit_autosplit_dinit_$method_num$();\n", "private static void _clinit_autosplit_dinit_$method_num$() {\n"); } + // Feature resolution for Java features uses extension registry + // which must happen after internalInit() from + // GenerateNonNestedInitializationCode + printer->Print("descriptor.resolveAllFeatures();\n"); // Proto compiler builds a DescriptorPool, which holds all the descriptors to // generate, when processing the ".proto" files. We call this DescriptorPool @@ -493,109 +495,6 @@ void FileGenerator::GenerateDescriptorInitializationCodeForImmutable( printer->Print("}\n"); } -void FileGenerator::GenerateDescriptorInitializationCodeForMutable( - io::Printer* printer) { - printer->Print( - "public static com.google.protobuf.Descriptors.FileDescriptor\n" - " getDescriptor() {\n" - " return descriptor;\n" - "}\n" - "private static final com.google.protobuf.Descriptors.FileDescriptor\n" - " descriptor;\n" - "static {\n"); - printer->Indent(); - - printer->Print( - "descriptor = $immutable_package$.$descriptor_classname$.descriptor;\n", - "immutable_package", FileJavaPackage(file_, true, options_), - "descriptor_classname", name_resolver_->GetDescriptorClassName(file_)); - - for (int i = 0; i < file_->message_type_count(); i++) { - message_generators_[i]->GenerateStaticVariableInitializers(printer); - } - for (int i = 0; i < file_->extension_count(); i++) { - extension_generators_[i]->GenerateNonNestedInitializationCode(printer); - } - - // Check if custom options exist. If any, try to load immutable classes since - // custom options are only represented with immutable messages. - FileDescriptorProto file_proto = StripSourceRetentionOptions(*file_); - std::string file_data; - file_proto.SerializeToString(&file_data); - FieldDescriptorSet extensions; - CollectExtensions(file_proto, *file_->pool(), &extensions, file_data); - - if (!extensions.empty()) { - // Try to load immutable messages' outer class. Its initialization code - // will take care of interpreting custom options. - printer->Print( - "try {\n" - // Note that we have to load the immutable class dynamically here as - // we want the mutable code to be independent from the immutable code - // at compile time. It is required to implement dual-compile for - // mutable and immutable API in blaze. - " java.lang.Class immutableClass = java.lang.Class.forName(\n" - " \"$immutable_classname$\");\n" - "} catch (java.lang.ClassNotFoundException e) {\n", - "immutable_classname", name_resolver_->GetImmutableClassName(file_)); - printer->Indent(); - - // The immutable class can not be found. We try our best to collect all - // custom option extensions to interpret the custom options. - printer->Print( - "com.google.protobuf.ExtensionRegistry registry =\n" - " com.google.protobuf.ExtensionRegistry.newInstance();\n" - "com.google.protobuf.MessageLite defaultExtensionInstance = null;\n"); - - for (const FieldDescriptor* field : extensions) { - std::string scope; - if (field->extension_scope() != NULL) { - scope = absl::StrCat( - name_resolver_->GetMutableClassName(field->extension_scope()), - ".getDescriptor()"); - } else { - scope = - absl::StrCat(FileJavaPackage(field->file(), true, options_), ".", - name_resolver_->GetDescriptorClassName(field->file()), - ".descriptor"); - } - if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) { - printer->Print( - "defaultExtensionInstance = com.google.protobuf.Internal\n" - " .getDefaultInstance(\"$class$\");\n" - "if (defaultExtensionInstance != null) {\n" - " registry.add(\n" - " $scope$.getExtensions().get($index$),\n" - " (com.google.protobuf.Message) defaultExtensionInstance);\n" - "}\n", - "scope", scope, "index", absl::StrCat(field->index()), "class", - name_resolver_->GetImmutableClassName(field->message_type())); - } else { - printer->Print("registry.add($scope$.getExtensions().get($index$));\n", - "scope", scope, "index", absl::StrCat(field->index())); - } - } - printer->Print( - "com.google.protobuf.Descriptors.FileDescriptor\n" - " .internalUpdateFileDescriptor(descriptor, registry);\n"); - - printer->Outdent(); - printer->Print("}\n"); - } - - // Force descriptor initialization of all dependencies. - for (int i = 0; i < file_->dependency_count(); i++) { - if (ShouldIncludeDependency(file_->dependency(i), false)) { - std::string dependency = - name_resolver_->GetMutableClassName(file_->dependency(i)); - printer->Print("$dependency$.getDescriptor();\n", "dependency", - dependency); - } - } - - printer->Outdent(); - printer->Print("}\n"); -} template static void GenerateSibling( diff --git a/src/google/protobuf/compiler/java/file.h b/src/google/protobuf/compiler/java/file.h index e8b11d3473060..ab13afcc2a189 100644 --- a/src/google/protobuf/compiler/java/file.h +++ b/src/google/protobuf/compiler/java/file.h @@ -78,7 +78,6 @@ class FileGenerator { private: void GenerateDescriptorInitializationCodeForImmutable(io::Printer* printer); - void GenerateDescriptorInitializationCodeForMutable(io::Printer* printer); bool ShouldIncludeDependency(const FileDescriptor* descriptor, bool immutable_api_); diff --git a/src/google/protobuf/compiler/java/generator.cc b/src/google/protobuf/compiler/java/generator.cc index 5435bb9e13911..aceb1207f13bf 100644 --- a/src/google/protobuf/compiler/java/generator.cc +++ b/src/google/protobuf/compiler/java/generator.cc @@ -38,7 +38,8 @@ JavaGenerator::JavaGenerator() {} JavaGenerator::~JavaGenerator() {} uint64_t JavaGenerator::GetSupportedFeatures() const { - return CodeGenerator::Feature::FEATURE_PROTO3_OPTIONAL; + return CodeGenerator::Feature::FEATURE_PROTO3_OPTIONAL | + CodeGenerator::Feature::FEATURE_SUPPORTS_EDITIONS; } bool JavaGenerator::Generate(const FileDescriptor* file, diff --git a/src/google/protobuf/compiler/java/shared_code_generator.cc b/src/google/protobuf/compiler/java/shared_code_generator.cc index b73c37ea655af..c77becc529c91 100644 --- a/src/google/protobuf/compiler/java/shared_code_generator.cc +++ b/src/google/protobuf/compiler/java/shared_code_generator.cc @@ -73,10 +73,22 @@ void SharedCodeGenerator::Generate( if (!options_.opensource_runtime) { printer->Print("@com.google.protobuf.Internal.ProtoNonnullApi\n"); } + printer->Print( + "public final class $classname$ {\n" + " /* This variable is to be called by generated code only. It " + "returns\n" + " * an incomplete descriptor for internal use only. */\n" " public static com.google.protobuf.Descriptors.FileDescriptor\n" " descriptor;\n" + " /* This method is to be called by generated code only. It returns\n" + " * an incomplete descriptor for internal use only. */\n" + " public static com.google.protobuf.Descriptors.FileDescriptor " + "getDescriptor() {\n" + " descriptor.resolveAllFeatures();\n" + " return descriptor;\n" + " }\n" " static {\n", "classname", classname); printer->Annotate("classname", file_->name()); diff --git a/src/google/protobuf/editions/BUILD b/src/google/protobuf/editions/BUILD index 96fc9b1385dc1..d4bd91316b9a7 100644 --- a/src/google/protobuf/editions/BUILD +++ b/src/google/protobuf/editions/BUILD @@ -109,6 +109,13 @@ internal_objc_proto_library( visibility = ["//conformance:__pkg__"], ) +java_proto_library( + name = "test_messages_proto2_editions_java_proto", + testonly = True, + visibility = ["//conformance:__pkg__"], + deps = [":test_messages_proto2_editions_proto"], +) + py_proto_library( name = "test_messages_proto2_editions_py_pb2", testonly = True, @@ -164,6 +171,13 @@ internal_objc_proto_library( visibility = ["//conformance:__pkg__"], ) +java_proto_library( + name = "test_messages_proto3_editions_java_proto", + testonly = True, + visibility = ["//conformance:__pkg__"], + deps = [":test_messages_proto3_editions_proto"], +) + py_proto_library( name = "test_messages_proto3_editions_py_pb2", testonly = True,