|
| 1 | +package com.google.protobuf; |
| 2 | + |
| 3 | +import com.google.protobuf.DescriptorProtos.DescriptorProto; |
| 4 | +import com.google.protobuf.DescriptorProtos.Edition; |
| 5 | +import com.google.protobuf.DescriptorProtos.EnumDescriptorProto; |
| 6 | +import com.google.protobuf.DescriptorProtos.FeatureSet; |
| 7 | +import com.google.protobuf.DescriptorProtos.FileDescriptorProtoOrBuilder; |
| 8 | +import com.google.protobuf.DescriptorProtos.FileOptions; |
| 9 | +import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto; |
| 10 | +import com.google.protobuf.Descriptors.Descriptor; |
| 11 | +import com.google.protobuf.Descriptors.EnumDescriptor; |
| 12 | +import com.google.protobuf.Descriptors.FileDescriptor; |
| 13 | +import com.google.protobuf.Descriptors.ServiceDescriptor; |
| 14 | +import com.google.protobuf.GeneratedMessage.GeneratedExtension; |
| 15 | +import com.google.protobuf.JavaFeaturesProto.JavaFeatures; |
| 16 | + |
| 17 | +/** Class containing helper methods for predicting names of generated java classes. */ |
| 18 | +public final class GeneratorNames { |
| 19 | + |
| 20 | + private GeneratorNames() {} |
| 21 | + |
| 22 | + /** Returns the generated package for the given file descriptor proto. */ |
| 23 | + public static String getFileJavaPackage(FileDescriptorProtoOrBuilder file) { |
| 24 | + return getProto2ApiDefaultJavaPackage(file.getOptions(), file.getPackage()); |
| 25 | + } |
| 26 | + |
| 27 | + /** Returns the generated package for the given file descriptor. */ |
| 28 | + public static String getFileJavaPackage(FileDescriptor file) { |
| 29 | + return getProto2ApiDefaultJavaPackage(file.getOptions(), file.getPackage()); |
| 30 | + } |
| 31 | + |
| 32 | + /** Returns the default java package for the given file. */ |
| 33 | + static String getDefaultJavaPackage(FileOptions fileOptions, String filePackage) { |
| 34 | + // Replicates the logic from DefaultJavaPackage in java/names_internal.h. |
| 35 | + if (fileOptions.hasJavaPackage()) { |
| 36 | + return fileOptions.getJavaPackage(); |
| 37 | + } else { |
| 38 | + return filePackage; |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + /** Joins two package segments into a single package name with a dot separator. */ |
| 43 | + static String joinPackage(String a, String b) { |
| 44 | + if (a.isEmpty()) { |
| 45 | + return b; |
| 46 | + } else if (b.isEmpty()) { |
| 47 | + return a; |
| 48 | + } else { |
| 49 | + return a + '.' + b; |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + /** |
| 54 | + * Returns the package name to use for a file that is being compiled as proto2-API. If the file is |
| 55 | + * declared as proto1-API, this may involve using the alternate package name. |
| 56 | + */ |
| 57 | + static String getProto2ApiDefaultJavaPackage(FileOptions fileOptions, String filePackage) { |
| 58 | + // Replicates the logic from Proto2DefaultJavaPackage in java/names_internal.h. |
| 59 | + return getDefaultJavaPackage(fileOptions, filePackage); |
| 60 | + } |
| 61 | + |
| 62 | + /** |
| 63 | + * Returns the generated unqualified outer file class name for the given file descriptor proto. |
| 64 | + */ |
| 65 | + public static String getFileClassName(FileDescriptorProtoOrBuilder file) { |
| 66 | + return getFileClassNameImpl(file, getResolvedFileFeatures(JavaFeaturesProto.java_, file)); |
| 67 | + } |
| 68 | + |
| 69 | + /** Returns the generated unqualified outer file class name for the given file descriptor. */ |
| 70 | + public static String getFileClassName(FileDescriptor file) { |
| 71 | + return getFileClassNameImpl( |
| 72 | + file.toProto(), file.getFeatures().getExtension(JavaFeaturesProto.java_)); |
| 73 | + } |
| 74 | + |
| 75 | + private static String getFileClassNameImpl( |
| 76 | + FileDescriptorProtoOrBuilder file, JavaFeatures resolvedFeatures) { |
| 77 | + // Replicates the logic of ClassNameResolver::GetFileImmutableClassName. |
| 78 | + if (file.getOptions().hasJavaOuterClassname()) { |
| 79 | + return file.getOptions().getJavaOuterClassname(); |
| 80 | + } |
| 81 | + |
| 82 | + String className = |
| 83 | + getDefaultFileClassName(file, resolvedFeatures.getUseOldOuterClassnameDefault()); |
| 84 | + if (resolvedFeatures.getUseOldOuterClassnameDefault() |
| 85 | + && hasConflictingClassName(file, className)) { |
| 86 | + return className + "OuterClass"; |
| 87 | + } |
| 88 | + return className; |
| 89 | + } |
| 90 | + |
| 91 | + /** |
| 92 | + * Returns the resolved features for the given descriptor proto. |
| 93 | + * |
| 94 | + * <p>This method isn't actually naming-specific, but lives here for now because it's the only |
| 95 | + * place it's needed. Once we have a better home for dealing with features in code generators, |
| 96 | + * this should move. |
| 97 | + */ |
| 98 | + @SuppressWarnings("unchecked") |
| 99 | + static <T extends Message> T getResolvedFileFeatures( |
| 100 | + GeneratedExtension<FeatureSet, T> ext, FileDescriptorProtoOrBuilder file) { |
| 101 | + Edition edition; |
| 102 | + if (file.getSyntax().equals("proto3")) { |
| 103 | + edition = Edition.EDITION_PROTO3; |
| 104 | + } else if (!file.hasEdition()) { |
| 105 | + edition = Edition.EDITION_PROTO2; |
| 106 | + } else { |
| 107 | + edition = file.getEdition(); |
| 108 | + } |
| 109 | + FeatureSet features = file.getOptions().getFeatures(); |
| 110 | + if (features.getUnknownFields().hasField(ext.getNumber())) { |
| 111 | + // The incoming descriptor proto may not have been parsed with the right extension registry, |
| 112 | + // in which case we need to parse it again with the right extensions. |
| 113 | + ExtensionRegistry registry = ExtensionRegistry.newInstance(); |
| 114 | + registry.add(ext); |
| 115 | + try { |
| 116 | + features = |
| 117 | + FeatureSet.newBuilder() |
| 118 | + .mergeFrom(features.getUnknownFields().toByteString(), registry) |
| 119 | + .build(); |
| 120 | + } catch (InvalidProtocolBufferException e) { |
| 121 | + // This should never happen, since the unknown fields have already been parsed and features |
| 122 | + // are always integral types. |
| 123 | + throw new IllegalArgumentException("Failed to parse features", e); |
| 124 | + } |
| 125 | + } |
| 126 | + return (T) |
| 127 | + Descriptors.getEditionDefaults(edition).getExtension(ext).toBuilder() |
| 128 | + .mergeFrom(features.getExtension(ext)) |
| 129 | + .build(); |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * Returns the default unqualified file class name for the given file. |
| 134 | + * |
| 135 | + * @param file the file descriptor proto |
| 136 | + * @param useOldOuterClassnameDefault whether to use the old default for the outer classname |
| 137 | + */ |
| 138 | + static String getDefaultFileClassName( |
| 139 | + FileDescriptorProtoOrBuilder file, boolean useOldOuterClassnameDefault) { |
| 140 | + // Replicates the logic of ClassNameResolver::GetFileDefaultImmutableClassName. |
| 141 | + String name = file.getName(); |
| 142 | + name = name.substring(name.lastIndexOf('/') + 1); |
| 143 | + name = underscoresToCamelCase(stripProto(name)); |
| 144 | + return useOldOuterClassnameDefault ? name : name + "Proto"; |
| 145 | + } |
| 146 | + |
| 147 | + private static String stripProto(String filename) { |
| 148 | + // Replicates the logic of ClassNameResolver::StripProto. |
| 149 | + if (filename.endsWith(".protodevel")) { |
| 150 | + return filename.substring(0, filename.length() - ".protodevel".length()); |
| 151 | + } |
| 152 | + if (filename.endsWith(".proto")) { |
| 153 | + return filename.substring(0, filename.length() - ".proto".length()); |
| 154 | + } |
| 155 | + |
| 156 | + return filename; |
| 157 | + } |
| 158 | + |
| 159 | + /** Checks whether any generated classes conflict with the given name. */ |
| 160 | + private static boolean hasConflictingClassName(FileDescriptorProtoOrBuilder file, String name) { |
| 161 | + for (EnumDescriptorProto enumDesc : file.getEnumTypeList()) { |
| 162 | + if (name.equals(enumDesc.getName())) { |
| 163 | + return true; |
| 164 | + } |
| 165 | + } |
| 166 | + for (ServiceDescriptorProto serviceDesc : file.getServiceList()) { |
| 167 | + if (name.equals(serviceDesc.getName())) { |
| 168 | + return true; |
| 169 | + } |
| 170 | + } |
| 171 | + for (DescriptorProto messageDesc : file.getMessageTypeList()) { |
| 172 | + if (hasConflictingClassName(messageDesc, name)) { |
| 173 | + return true; |
| 174 | + } |
| 175 | + } |
| 176 | + return false; |
| 177 | + } |
| 178 | + |
| 179 | + /** Used by the other overload, descends recursively into messages. */ |
| 180 | + private static boolean hasConflictingClassName(DescriptorProto messageDesc, String name) { |
| 181 | + if (name.equals(messageDesc.getName())) { |
| 182 | + return true; |
| 183 | + } |
| 184 | + for (EnumDescriptorProto enumDesc : messageDesc.getEnumTypeList()) { |
| 185 | + if (name.equals(enumDesc.getName())) { |
| 186 | + return true; |
| 187 | + } |
| 188 | + } |
| 189 | + for (DescriptorProto nestedMessageDesc : messageDesc.getNestedTypeList()) { |
| 190 | + if (hasConflictingClassName(nestedMessageDesc, name)) { |
| 191 | + return true; |
| 192 | + } |
| 193 | + } |
| 194 | + return false; |
| 195 | + } |
| 196 | + |
| 197 | + /** |
| 198 | + * Converts a name to camel-case. |
| 199 | + * |
| 200 | + * @param input the input string to convert |
| 201 | + * @param capitalizeNextLetter whether to capitalize the first letter |
| 202 | + */ |
| 203 | + static String underscoresToCamelCase(String input, boolean capitalizeNextLetter) { |
| 204 | + // Replicates the logic of ClassNameResolver::UnderscoresToCamelCase. |
| 205 | + StringBuilder result = new StringBuilder(); |
| 206 | + for (int i = 0; i < input.length(); i++) { |
| 207 | + char ch = input.charAt(i); |
| 208 | + if ('a' <= ch && ch <= 'z') { |
| 209 | + if (capitalizeNextLetter) { |
| 210 | + result.append((char) (ch + ('A' - 'a'))); |
| 211 | + } else { |
| 212 | + result.append(ch); |
| 213 | + } |
| 214 | + capitalizeNextLetter = false; |
| 215 | + } else if ('A' <= ch && ch <= 'Z') { |
| 216 | + if (i == 0 && !capitalizeNextLetter) { |
| 217 | + // Force first letter to lower-case unless explicitly told to |
| 218 | + // capitalize it. |
| 219 | + result.append((char) (ch + ('a' - 'A'))); |
| 220 | + } else { |
| 221 | + // Capital letters after the first are left as-is. |
| 222 | + result.append(ch); |
| 223 | + } |
| 224 | + capitalizeNextLetter = false; |
| 225 | + } else if ('0' <= ch && ch <= '9') { |
| 226 | + result.append(ch); |
| 227 | + capitalizeNextLetter = true; |
| 228 | + } else { |
| 229 | + capitalizeNextLetter = true; |
| 230 | + } |
| 231 | + } |
| 232 | + return result.toString(); |
| 233 | + } |
| 234 | + |
| 235 | + static String underscoresToCamelCase(String input) { |
| 236 | + return underscoresToCamelCase(input, /* capitalizeNextLetter= */ true); |
| 237 | + } |
| 238 | + |
| 239 | + /** Returns the fully qualified Java class name for the given message descriptor. */ |
| 240 | + public static String getClassName(Descriptor message) { |
| 241 | + // Replicates the logic for ClassName from immutable/names.h |
| 242 | + return getClassFullName( |
| 243 | + getClassNameWithoutPackage(message), message.getFile(), !getNestInFileClass(message)); |
| 244 | + } |
| 245 | + |
| 246 | + /** Returns the fully qualified Java class name for the given enum descriptor. */ |
| 247 | + public static String getClassName(EnumDescriptor enm) { |
| 248 | + // Replicates the logic for ClassName from immutable/names.h |
| 249 | + return getClassFullName( |
| 250 | + getClassNameWithoutPackage(enm), enm.getFile(), !getNestInFileClass(enm)); |
| 251 | + } |
| 252 | + |
| 253 | + /** Returns the fully qualified Java class name for the given service descriptor. */ |
| 254 | + static String getClassName(ServiceDescriptor service) { |
| 255 | + // Replicates the logic for ClassName from immutable/names.h |
| 256 | + String suffix = ""; |
| 257 | + boolean isOwnFile = !getNestInFileClass(service); |
| 258 | + return getClassFullName(getClassNameWithoutPackage(service), service.getFile(), isOwnFile) |
| 259 | + + suffix; |
| 260 | + } |
| 261 | + |
| 262 | + private static String getClassFullName( |
| 263 | + String nameWithoutPackage, FileDescriptor file, boolean isOwnFile) { |
| 264 | + // Replicates the logic for ClassNameResolver::GetJavaClassFullName from immutable/names.cc |
| 265 | + StringBuilder result = new StringBuilder(); |
| 266 | + if (isOwnFile) { |
| 267 | + result.append(getFileJavaPackage(file.toProto())); |
| 268 | + if (result.length() > 0) { |
| 269 | + result.append("."); |
| 270 | + } |
| 271 | + } else { |
| 272 | + result.append(joinPackage(getFileJavaPackage(file.toProto()), getFileClassName(file))); |
| 273 | + if (result.length() > 0) { |
| 274 | + result.append("$"); |
| 275 | + } |
| 276 | + } |
| 277 | + result.append(nameWithoutPackage.replace('.', '$')); |
| 278 | + return result.toString(); |
| 279 | + } |
| 280 | + |
| 281 | + /** Returns the nest_in_file_class behavior for a given set of features in a specific file. */ |
| 282 | + // Switch expressions were released in Java 14, and we support Java 8. |
| 283 | + @SuppressWarnings("StatementSwitchToExpressionSwitch") |
| 284 | + private static boolean getNestInFileClass(FileDescriptor file, JavaFeatures resolvedFeatures) { |
| 285 | + switch (resolvedFeatures.getNestInFileClass()) { |
| 286 | + case YES: |
| 287 | + return true; |
| 288 | + case NO: |
| 289 | + return false; |
| 290 | + case LEGACY: |
| 291 | + return !file.getOptions().getJavaMultipleFiles(); |
| 292 | + default: |
| 293 | + throw new IllegalArgumentException("Java features are not resolved"); |
| 294 | + } |
| 295 | + } |
| 296 | + |
| 297 | + private static boolean getNestInFileClass(Descriptor descriptor) { |
| 298 | + return getNestInFileClass( |
| 299 | + descriptor.getFile(), descriptor.getFeatures().getExtension(JavaFeaturesProto.java_)); |
| 300 | + } |
| 301 | + |
| 302 | + private static boolean getNestInFileClass(EnumDescriptor descriptor) { |
| 303 | + return getNestInFileClass( |
| 304 | + descriptor.getFile(), descriptor.getFeatures().getExtension(JavaFeaturesProto.java_)); |
| 305 | + } |
| 306 | + |
| 307 | + private static boolean getNestInFileClass(ServiceDescriptor descriptor) { |
| 308 | + return getNestInFileClass( |
| 309 | + descriptor.getFile(), descriptor.getFeatures().getExtension(JavaFeaturesProto.java_)); |
| 310 | + } |
| 311 | + |
| 312 | + /** Returns the name of the given descriptor without the package name prefix. */ |
| 313 | + static String stripPackageName(String fullName, FileDescriptor file) { |
| 314 | + if (file.getPackage().isEmpty()) { |
| 315 | + return fullName; |
| 316 | + } else { |
| 317 | + return fullName.substring(file.getPackage().length() + 1); |
| 318 | + } |
| 319 | + } |
| 320 | + |
| 321 | + /** Returns the name of the given message descriptor without the package name prefix. */ |
| 322 | + static String getClassNameWithoutPackage(Descriptor message) { |
| 323 | + return stripPackageName(message.getFullName(), message.getFile()); |
| 324 | + } |
| 325 | + |
| 326 | + /** Returns the name of the given enum descriptor without the package name prefix. */ |
| 327 | + static String getClassNameWithoutPackage(EnumDescriptor enm) { |
| 328 | + Descriptor containingType = enm.getContainingType(); |
| 329 | + if (containingType == null) { |
| 330 | + return enm.getName(); |
| 331 | + } |
| 332 | + return joinPackage(getClassNameWithoutPackage(containingType), enm.getName()); |
| 333 | + } |
| 334 | + |
| 335 | + /** Returns the name of the given service descriptor without the package name prefix. */ |
| 336 | + static String getClassNameWithoutPackage(ServiceDescriptor service) { |
| 337 | + return stripPackageName(service.getFullName(), service.getFile()); |
| 338 | + } |
| 339 | +} |
0 commit comments