1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.google.api.generator.gapic.protoparser; 16 17 import com.google.api.ClientProto; 18 import com.google.api.DocumentationRule; 19 import com.google.api.HttpRule; 20 import com.google.api.ResourceDescriptor; 21 import com.google.api.ResourceProto; 22 import com.google.api.generator.engine.ast.TypeNode; 23 import com.google.api.generator.engine.ast.VaporReference; 24 import com.google.api.generator.gapic.model.Field; 25 import com.google.api.generator.gapic.model.GapicBatchingSettings; 26 import com.google.api.generator.gapic.model.GapicContext; 27 import com.google.api.generator.gapic.model.GapicLanguageSettings; 28 import com.google.api.generator.gapic.model.GapicLroRetrySettings; 29 import com.google.api.generator.gapic.model.GapicServiceConfig; 30 import com.google.api.generator.gapic.model.HttpBindings; 31 import com.google.api.generator.gapic.model.LongrunningOperation; 32 import com.google.api.generator.gapic.model.Message; 33 import com.google.api.generator.gapic.model.Method; 34 import com.google.api.generator.gapic.model.OperationResponse; 35 import com.google.api.generator.gapic.model.ResourceName; 36 import com.google.api.generator.gapic.model.ResourceReference; 37 import com.google.api.generator.gapic.model.RoutingHeaderRule; 38 import com.google.api.generator.gapic.model.Service; 39 import com.google.api.generator.gapic.model.SourceCodeInfoLocation; 40 import com.google.api.generator.gapic.model.Transport; 41 import com.google.api.generator.gapic.utils.ResourceNameConstants; 42 import com.google.cloud.ExtendedOperationsProto; 43 import com.google.cloud.OperationResponseMapping; 44 import com.google.common.annotations.VisibleForTesting; 45 import com.google.common.base.Preconditions; 46 import com.google.common.base.Strings; 47 import com.google.common.collect.BiMap; 48 import com.google.common.collect.HashBiMap; 49 import com.google.common.collect.ImmutableSet; 50 import com.google.common.collect.Maps; 51 import com.google.longrunning.OperationInfo; 52 import com.google.longrunning.OperationsProto; 53 import com.google.protobuf.DescriptorProtos.FieldOptions; 54 import com.google.protobuf.DescriptorProtos.FileDescriptorProto; 55 import com.google.protobuf.DescriptorProtos.MessageOptions; 56 import com.google.protobuf.DescriptorProtos.MethodOptions; 57 import com.google.protobuf.DescriptorProtos.ServiceOptions; 58 import com.google.protobuf.Descriptors.Descriptor; 59 import com.google.protobuf.Descriptors.DescriptorValidationException; 60 import com.google.protobuf.Descriptors.EnumDescriptor; 61 import com.google.protobuf.Descriptors.EnumValueDescriptor; 62 import com.google.protobuf.Descriptors.FieldDescriptor; 63 import com.google.protobuf.Descriptors.FileDescriptor; 64 import com.google.protobuf.Descriptors.MethodDescriptor; 65 import com.google.protobuf.Descriptors.ServiceDescriptor; 66 import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.Collection; 70 import java.util.Collections; 71 import java.util.HashMap; 72 import java.util.HashSet; 73 import java.util.List; 74 import java.util.Map; 75 import java.util.Objects; 76 import java.util.Optional; 77 import java.util.Set; 78 import java.util.function.Function; 79 import java.util.stream.Collectors; 80 import java.util.stream.IntStream; 81 82 public class Parser { 83 private static final String COMMA = ","; 84 private static final String COLON = ":"; 85 private static final String DEFAULT_PORT = "443"; 86 private static final String DOT = "."; 87 private static final String SLASH = "/"; 88 89 private static final ResourceName WILDCARD_RESOURCE_NAME = 90 ResourceName.createWildcard("*", "com.google.api.wildcard.placeholder"); 91 92 // Mirrors the sanitizer allowlist. 93 private static final Set<String> MIXIN_ALLOWLIST = 94 ImmutableSet.of( 95 "google.iam.v1.IAMPolicy", 96 "google.longrunning.Operations", 97 "google.cloud.location.Locations"); 98 // These must be kept in sync with the above protos' java_package options. 99 private static final Set<String> MIXIN_JAVA_PACKAGE_ALLOWLIST = 100 ImmutableSet.of("com.google.iam.v1", "com.google.longrunning", "com.google.cloud.location"); 101 102 // Allow other parsers to access this. 103 protected static final SourceCodeInfoParser SOURCE_CODE_INFO_PARSER = new SourceCodeInfoParser(); 104 105 static class GapicParserException extends RuntimeException { GapicParserException(String errorMessage)106 public GapicParserException(String errorMessage) { 107 super(errorMessage); 108 } 109 } 110 parse(CodeGeneratorRequest request)111 public static GapicContext parse(CodeGeneratorRequest request) { 112 Optional<String> gapicYamlConfigPathOpt = 113 PluginArgumentParser.parseGapicYamlConfigPath(request); 114 Optional<List<GapicBatchingSettings>> batchingSettingsOpt = 115 BatchingSettingsConfigParser.parse(gapicYamlConfigPathOpt); 116 Optional<List<GapicLroRetrySettings>> lroRetrySettingsOpt = 117 GapicLroRetrySettingsParser.parse(gapicYamlConfigPathOpt); 118 Optional<GapicLanguageSettings> languageSettingsOpt = 119 GapicLanguageSettingsParser.parse(gapicYamlConfigPathOpt); 120 Optional<String> transportOpt = PluginArgumentParser.parseTransport(request); 121 122 boolean willGenerateMetadata = PluginArgumentParser.hasMetadataFlag(request); 123 boolean willGenerateNumericEnum = PluginArgumentParser.hasNumericEnumFlag(request); 124 125 Optional<String> serviceConfigPathOpt = PluginArgumentParser.parseJsonConfigPath(request); 126 Optional<GapicServiceConfig> serviceConfigOpt = 127 ServiceConfigParser.parse(serviceConfigPathOpt.orElse(null)); 128 if (serviceConfigOpt.isPresent()) { 129 GapicServiceConfig serviceConfig = serviceConfigOpt.get(); 130 serviceConfig.setLroRetrySettings(lroRetrySettingsOpt); 131 serviceConfig.setBatchingSettings(batchingSettingsOpt); 132 serviceConfig.setLanguageSettings(languageSettingsOpt); 133 serviceConfigOpt = Optional.of(serviceConfig); 134 } 135 136 Optional<String> serviceYamlConfigPathOpt = 137 PluginArgumentParser.parseServiceYamlConfigPath(request); 138 Optional<com.google.api.Service> serviceYamlProtoOpt = 139 serviceYamlConfigPathOpt.flatMap(ServiceYamlParser::parse); 140 141 // Collect the resource references seen in messages. 142 Set<ResourceReference> outputResourceReferencesSeen = new HashSet<>(); 143 // Keep message and resource name parsing separate for cleaner logic. 144 // While this takes an extra pass through the protobufs, the extra time is relatively trivial 145 // and is worth the larger reduced maintenance cost. 146 Map<String, Message> messages = parseMessages(request, outputResourceReferencesSeen); 147 148 Map<String, ResourceName> resourceNames = parseResourceNames(request); 149 messages = updateResourceNamesInMessages(messages, resourceNames.values()); 150 151 // Contains only resource names that are actually used. Usage refers to the presence of a 152 // request message's field in an RPC's method_signature annotation. That is, resource name 153 // definitions 154 // or references that are simply defined, but not used in such a manner, will not have 155 // corresponding Java helper 156 // classes generated. 157 Set<ResourceName> outputArgResourceNames = new HashSet<>(); 158 List<Service> mixinServices = new ArrayList<>(); 159 Transport transport = Transport.parse(transportOpt.orElse(Transport.GRPC.toString())); 160 List<Service> services = 161 parseServices( 162 request, 163 messages, 164 resourceNames, 165 outputArgResourceNames, 166 serviceYamlProtoOpt, 167 serviceConfigOpt, 168 mixinServices, 169 transport); 170 171 Preconditions.checkState(!services.isEmpty(), "No services found to generate"); 172 173 // TODO(vam-google): Figure out whether we should keep this allowlist or bring 174 // back the unused resource names for all APIs. 175 // Temporary workaround for Ads, who still need these resource names. 176 if (services.get(0).protoPakkage().startsWith("google.ads.googleads.v")) { 177 Function<ResourceName, String> typeNameFn = 178 r -> r.resourceTypeString().substring(r.resourceTypeString().indexOf("/") + 1); 179 Function<Set<ResourceName>, Set<String>> typeStringSetFn = 180 sr -> sr.stream().map(typeNameFn).collect(Collectors.toSet()); 181 182 // Include all resource names present in message types for backwards-compatibility with the 183 // monolith. In the future, this should be removed on a client library major semver update. 184 // Resolve type name collisions with the ones present in the method arguments. 185 final Set<String> typeStringSet = typeStringSetFn.apply(outputArgResourceNames); 186 outputArgResourceNames.addAll( 187 resourceNames.values().stream() 188 .filter(r -> r.hasParentMessageName() && !typeStringSet.contains(typeNameFn.apply(r))) 189 .collect(Collectors.toSet())); 190 191 String servicePackage = services.get(0).pakkage(); 192 Map<String, ResourceName> patternsToResourceNames = 193 ResourceParserHelpers.createPatternResourceNameMap(resourceNames); 194 for (ResourceReference resourceReference : outputResourceReferencesSeen) { 195 final Set<String> interimTypeStringSet = typeStringSetFn.apply(outputArgResourceNames); 196 outputArgResourceNames.addAll( 197 ResourceReferenceParser.parseResourceNames( 198 resourceReference, servicePackage, null, resourceNames, patternsToResourceNames) 199 .stream() 200 .filter(r -> !interimTypeStringSet.contains(typeNameFn.apply(r))) 201 .collect(Collectors.toSet())); 202 } 203 } 204 205 return GapicContext.builder() 206 .setServices(services) 207 .setMixinServices( 208 // Mixin classes must share the package with the service they are mixed in, instead of 209 // their original package 210 mixinServices.stream() 211 .map(s -> s.toBuilder().setPakkage(services.get(0).pakkage()).build()) 212 .collect(Collectors.toList())) 213 .setMessages(messages) 214 .setResourceNames(resourceNames) 215 .setHelperResourceNames(outputArgResourceNames) 216 .setServiceConfig(serviceConfigOpt.orElse(null)) 217 .setGapicMetadataEnabled(willGenerateMetadata) 218 .setServiceYamlProto(serviceYamlProtoOpt.orElse(null)) 219 .setTransport(transport) 220 .setRestNumericEnumsEnabled(willGenerateNumericEnum) 221 .build(); 222 } 223 parseServices( CodeGeneratorRequest request, Map<String, Message> messageTypes, Map<String, ResourceName> resourceNames, Set<ResourceName> outputArgResourceNames, Optional<com.google.api.Service> serviceYamlProtoOpt, Optional<GapicServiceConfig> serviceConfigOpt, List<Service> outputMixinServices, Transport transport)224 public static List<Service> parseServices( 225 CodeGeneratorRequest request, 226 Map<String, Message> messageTypes, 227 Map<String, ResourceName> resourceNames, 228 Set<ResourceName> outputArgResourceNames, 229 Optional<com.google.api.Service> serviceYamlProtoOpt, 230 Optional<GapicServiceConfig> serviceConfigOpt, 231 List<Service> outputMixinServices, 232 Transport transport) { 233 Map<String, FileDescriptor> fileDescriptors = getFilesToGenerate(request); 234 235 List<Service> services = new ArrayList<>(); 236 for (String fileToGenerate : request.getFileToGenerateList()) { 237 FileDescriptor fileDescriptor = 238 Preconditions.checkNotNull( 239 fileDescriptors.get(fileToGenerate), 240 "Missing file descriptor for [%s]", 241 fileToGenerate); 242 243 services.addAll( 244 parseService( 245 fileDescriptor, 246 messageTypes, 247 resourceNames, 248 serviceYamlProtoOpt, 249 serviceConfigOpt, 250 outputArgResourceNames, 251 transport)); 252 } 253 254 // Prevent codegen for mixed-in services if there are other services present, since that is an 255 // indicator that we are not generating a GAPIC client for the mixed-in service on its own. 256 Function<Service, String> serviceFullNameFn = 257 s -> String.format("%s.%s", s.protoPakkage(), s.name()); 258 Set<Service> blockedCodegenMixinApis = new HashSet<>(); 259 Set<Service> definedServices = new HashSet<>(); 260 for (Service s : services) { 261 if (MIXIN_ALLOWLIST.contains(serviceFullNameFn.apply(s))) { 262 blockedCodegenMixinApis.add(s); 263 } else { 264 definedServices.add(s); 265 } 266 } 267 // It's very unlikely the blocklisted APIs will contain the other, or any other service. 268 boolean servicesContainBlocklistedApi = 269 !blockedCodegenMixinApis.isEmpty() && !definedServices.isEmpty(); 270 // Service names that are stated in the YAML file (as mixins). Used to filter 271 // blockedCodegenMixinApis. 272 Set<String> mixedInApis = 273 !serviceYamlProtoOpt.isPresent() 274 ? Collections.emptySet() 275 : serviceYamlProtoOpt.get().getApisList().stream() 276 .filter(a -> MIXIN_ALLOWLIST.contains(a.getName())) 277 .map(a -> a.getName()) 278 .collect(Collectors.toSet()); 279 // Holds the methods to be mixed in. 280 // Key: proto_package.ServiceName.RpcName. 281 // Value: HTTP rules, which clobber those in the proto. 282 // Assumes that http.rules.selector always specifies RPC names in the above format. 283 Map<String, HttpBindings> mixedInMethodsToHttpRules = new HashMap<>(); 284 Map<String, String> mixedInMethodsToDocs = new HashMap<>(); 285 // Parse HTTP rules and documentation, which will override the proto. 286 if (serviceYamlProtoOpt.isPresent()) { 287 for (HttpRule httpRule : serviceYamlProtoOpt.get().getHttp().getRulesList()) { 288 HttpBindings httpBindings = HttpRuleParser.parseHttpRule(httpRule); 289 if (httpBindings == null) { 290 continue; 291 } 292 for (String rpcFullNameRaw : httpRule.getSelector().split(",")) { 293 String rpcFullName = rpcFullNameRaw.trim(); 294 mixedInMethodsToHttpRules.put(rpcFullName, httpBindings); 295 } 296 } 297 for (DocumentationRule docRule : 298 serviceYamlProtoOpt.get().getDocumentation().getRulesList()) { 299 for (String rpcFullNameRaw : docRule.getSelector().split(",")) { 300 String rpcFullName = rpcFullNameRaw.trim(); 301 mixedInMethodsToDocs.put(rpcFullName, docRule.getDescription()); 302 } 303 } 304 } 305 306 // Sort potential mixin services alphabetically. 307 List<Service> orderedBlockedCodegenMixinApis = 308 blockedCodegenMixinApis.stream() 309 .sorted((s1, s2) -> s2.name().compareTo(s1.name())) 310 .collect(Collectors.toList()); 311 312 Set<String> apiDefinedRpcs = new HashSet<>(); 313 for (Service service : services) { 314 if (orderedBlockedCodegenMixinApis.contains(service)) { 315 continue; 316 } 317 apiDefinedRpcs.addAll( 318 service.methods().stream().map(m -> m.name()).collect(Collectors.toSet())); 319 } 320 // Mix-in APIs only if the protos are present and they're defined in the service.yaml file. 321 Set<Service> outputMixinServiceSet = new HashSet<>(); 322 if (servicesContainBlocklistedApi && !mixedInApis.isEmpty()) { 323 for (int i = 0; i < services.size(); i++) { 324 Service originalService = services.get(i); 325 List<Method> updatedOriginalServiceMethods = new ArrayList<>(originalService.methods()); 326 // If mixin APIs are present, add the methods to all other services. 327 for (Service mixinService : orderedBlockedCodegenMixinApis) { 328 final String mixinServiceFullName = serviceFullNameFn.apply(mixinService); 329 if (!mixedInApis.contains(mixinServiceFullName)) { 330 continue; 331 } 332 Function<Method, String> methodToFullProtoNameFn = 333 m -> String.format("%s.%s", mixinServiceFullName, m.name()); 334 // Filter mixed-in methods based on those listed in the HTTP rules section of 335 // service.yaml. 336 List<Method> updatedMixinMethods = 337 mixinService.methods().stream() 338 // Mixin method inclusion is based on the HTTP rules list in service.yaml. 339 .filter( 340 m -> mixedInMethodsToHttpRules.containsKey(methodToFullProtoNameFn.apply(m))) 341 .map( 342 m -> { 343 // HTTP rules and RPC documentation in the service.yaml file take 344 // precedence. 345 String fullMethodName = methodToFullProtoNameFn.apply(m); 346 HttpBindings httpBindings = 347 mixedInMethodsToHttpRules.containsKey(fullMethodName) 348 ? mixedInMethodsToHttpRules.get(fullMethodName) 349 : m.httpBindings(); 350 String docs = 351 mixedInMethodsToDocs.containsKey(fullMethodName) 352 ? mixedInMethodsToDocs.get(fullMethodName) 353 : m.description(); 354 return m.toBuilder() 355 .setHttpBindings(httpBindings) 356 .setDescription(docs) 357 .build(); 358 }) 359 .collect(Collectors.toList()); 360 // Overridden RPCs defined in the protos take precedence. 361 updatedMixinMethods.stream() 362 .filter(m -> !apiDefinedRpcs.contains(m.name())) 363 .forEach( 364 m -> 365 updatedOriginalServiceMethods.add( 366 m.toBuilder() 367 .setMixedInApiName(serviceFullNameFn.apply(mixinService)) 368 .build())); 369 // Sort by method name, to ensure a deterministic method ordering (for tests). 370 updatedMixinMethods = 371 updatedMixinMethods.stream() 372 .sorted((m1, m2) -> m2.name().compareTo(m1.name())) 373 .collect(Collectors.toList()); 374 outputMixinServiceSet.add( 375 mixinService.toBuilder().setMethods(updatedMixinMethods).build()); 376 } 377 services.set( 378 i, originalService.toBuilder().setMethods(updatedOriginalServiceMethods).build()); 379 } 380 } 381 382 if (servicesContainBlocklistedApi) { 383 services = 384 services.stream() 385 .filter(s -> !MIXIN_ALLOWLIST.contains(serviceFullNameFn.apply(s))) 386 .collect(Collectors.toList()); 387 } 388 389 // Use a list to ensure ordering for deterministic tests. 390 outputMixinServices.addAll( 391 outputMixinServiceSet.stream() 392 .sorted((s1, s2) -> s2.name().compareTo(s1.name())) 393 .collect(Collectors.toList())); 394 return services; 395 } 396 397 @VisibleForTesting parseService( FileDescriptor fileDescriptor, Map<String, Message> messageTypes, Map<String, ResourceName> resourceNames, Optional<com.google.api.Service> serviceYamlProtoOpt, Set<ResourceName> outputArgResourceNames)398 public static List<Service> parseService( 399 FileDescriptor fileDescriptor, 400 Map<String, Message> messageTypes, 401 Map<String, ResourceName> resourceNames, 402 Optional<com.google.api.Service> serviceYamlProtoOpt, 403 Set<ResourceName> outputArgResourceNames) { 404 return parseService( 405 fileDescriptor, 406 messageTypes, 407 resourceNames, 408 serviceYamlProtoOpt, 409 Optional.empty(), 410 outputArgResourceNames, 411 Transport.GRPC); 412 } 413 parseService( FileDescriptor fileDescriptor, Map<String, Message> messageTypes, Map<String, ResourceName> resourceNames, Optional<com.google.api.Service> serviceYamlProtoOpt, Optional<GapicServiceConfig> serviceConfigOpt, Set<ResourceName> outputArgResourceNames, Transport transport)414 public static List<Service> parseService( 415 FileDescriptor fileDescriptor, 416 Map<String, Message> messageTypes, 417 Map<String, ResourceName> resourceNames, 418 Optional<com.google.api.Service> serviceYamlProtoOpt, 419 Optional<GapicServiceConfig> serviceConfigOpt, 420 Set<ResourceName> outputArgResourceNames, 421 Transport transport) { 422 423 return fileDescriptor.getServices().stream() 424 .map( 425 s -> { 426 // Workaround for a missing default_host and oauth_scopes annotation from a service 427 // definition. This can happen for protos that bypass the publishing process. 428 // TODO(miraleung): Remove this workaround later? 429 ServiceOptions serviceOptions = s.getOptions(); 430 String defaultHost = null; 431 if (serviceOptions.hasExtension(ClientProto.defaultHost)) { 432 defaultHost = 433 sanitizeDefaultHost(serviceOptions.getExtension(ClientProto.defaultHost)); 434 } else if (serviceYamlProtoOpt.isPresent()) { 435 // Fall back to the DNS name supplied in the service .yaml config. 436 defaultHost = serviceYamlProtoOpt.get().getName(); 437 } 438 Preconditions.checkState( 439 !Strings.isNullOrEmpty(defaultHost), 440 String.format( 441 "Default host not found in service YAML config file or annotation for %s", 442 s.getName())); 443 444 List<String> oauthScopes = Collections.emptyList(); 445 if (serviceOptions.hasExtension(ClientProto.oauthScopes)) { 446 oauthScopes = 447 Arrays.asList( 448 serviceOptions.getExtension(ClientProto.oauthScopes).split(COMMA)); 449 } 450 451 boolean isDeprecated = false; 452 if (serviceOptions.hasDeprecated()) { 453 isDeprecated = serviceOptions.getDeprecated(); 454 } 455 456 Service.Builder serviceBuilder = Service.builder(); 457 if (fileDescriptor.toProto().hasSourceCodeInfo()) { 458 SourceCodeInfoLocation protoServiceLocation = 459 SOURCE_CODE_INFO_PARSER.getLocation(s); 460 if (!Objects.isNull(protoServiceLocation) 461 && !Strings.isNullOrEmpty(protoServiceLocation.getLeadingComments())) { 462 serviceBuilder.setDescription(protoServiceLocation.getLeadingComments()); 463 } 464 } 465 466 String serviceName = s.getName(); 467 String overriddenServiceName = serviceName; 468 String pakkage = TypeParser.getPackage(fileDescriptor); 469 String originalJavaPackage = pakkage; 470 // Override Java package with that specified in gapic.yaml. 471 if (serviceConfigOpt.isPresent() 472 && serviceConfigOpt.get().getLanguageSettingsOpt().isPresent()) { 473 GapicLanguageSettings languageSettings = 474 serviceConfigOpt.get().getLanguageSettingsOpt().get(); 475 pakkage = languageSettings.pakkage(); 476 overriddenServiceName = 477 languageSettings.getJavaServiceName(fileDescriptor.getPackage(), s.getName()); 478 } 479 return serviceBuilder 480 .setName(serviceName) 481 .setOverriddenName(overriddenServiceName) 482 .setDefaultHost(defaultHost) 483 .setOauthScopes(oauthScopes) 484 .setPakkage(pakkage) 485 .setOriginalJavaPackage(originalJavaPackage) 486 .setProtoPakkage(fileDescriptor.getPackage()) 487 .setIsDeprecated(isDeprecated) 488 .setMethods( 489 parseMethods( 490 s, 491 pakkage, 492 messageTypes, 493 resourceNames, 494 serviceConfigOpt, 495 outputArgResourceNames, 496 transport)) 497 .build(); 498 }) 499 .collect(Collectors.toList()); 500 } 501 parseMessages( CodeGeneratorRequest request, Set<ResourceReference> outputResourceReferencesSeen)502 public static Map<String, Message> parseMessages( 503 CodeGeneratorRequest request, Set<ResourceReference> outputResourceReferencesSeen) { 504 Map<String, FileDescriptor> fileDescriptors = getFilesToGenerate(request); 505 Map<String, Message> messages = new HashMap<>(); 506 // Look for message types amongst all the protos, not just the ones to generate. This will 507 // ensure we track commonly-used protos like Empty. 508 for (FileDescriptor fileDescriptor : fileDescriptors.values()) { 509 messages.putAll(parseMessages(fileDescriptor, outputResourceReferencesSeen)); 510 } 511 512 return messages; 513 } 514 515 // TODO(miraleung): Propagate the internal method to all tests, and remove this wrapper. parseMessages(FileDescriptor fileDescriptor)516 public static Map<String, Message> parseMessages(FileDescriptor fileDescriptor) { 517 return parseMessages(fileDescriptor, new HashSet<>()); 518 } 519 parseMessages( FileDescriptor fileDescriptor, Set<ResourceReference> outputResourceReferencesSeen)520 public static Map<String, Message> parseMessages( 521 FileDescriptor fileDescriptor, Set<ResourceReference> outputResourceReferencesSeen) { 522 // TODO(miraleung): Preserve nested type and package data in the type key. 523 Map<String, Message> messages = new HashMap<>(); 524 for (Descriptor messageDescriptor : fileDescriptor.getMessageTypes()) { 525 messages.putAll(parseMessages(messageDescriptor, outputResourceReferencesSeen)); 526 } 527 // We treat enums as messages since we primarily care only about the type representation. 528 for (EnumDescriptor enumDescriptor : fileDescriptor.getEnumTypes()) { 529 String name = enumDescriptor.getName(); 530 List<EnumValueDescriptor> valueDescriptors = enumDescriptor.getValues(); 531 TypeNode enumType = TypeParser.parseType(enumDescriptor); 532 messages.put( 533 enumType.reference().fullName(), 534 Message.builder() 535 .setType(enumType) 536 .setName(name) 537 .setFullProtoName(enumDescriptor.getFullName()) 538 .setEnumValues( 539 valueDescriptors.stream().map(v -> v.getName()).collect(Collectors.toList()), 540 valueDescriptors.stream().map(v -> v.getNumber()).collect(Collectors.toList())) 541 .build()); 542 } 543 return messages; 544 } 545 parseMessages( Descriptor messageDescriptor, Set<ResourceReference> outputResourceReferencesSeen)546 private static Map<String, Message> parseMessages( 547 Descriptor messageDescriptor, Set<ResourceReference> outputResourceReferencesSeen) { 548 return parseMessages(messageDescriptor, outputResourceReferencesSeen, new ArrayList<>()); 549 } 550 parseMessages( Descriptor messageDescriptor, Set<ResourceReference> outputResourceReferencesSeen, List<String> outerNestedTypes)551 private static Map<String, Message> parseMessages( 552 Descriptor messageDescriptor, 553 Set<ResourceReference> outputResourceReferencesSeen, 554 List<String> outerNestedTypes) { 555 Map<String, Message> messages = new HashMap<>(); 556 String messageName = messageDescriptor.getName(); 557 if (!messageDescriptor.getNestedTypes().isEmpty()) { 558 for (Descriptor nestedMessage : messageDescriptor.getNestedTypes()) { 559 if (isMapType(nestedMessage)) { 560 continue; 561 } 562 List<String> currentNestedTypes = new ArrayList<>(outerNestedTypes); 563 currentNestedTypes.add(messageName); 564 messages.putAll( 565 parseMessages(nestedMessage, outputResourceReferencesSeen, currentNestedTypes)); 566 } 567 } 568 TypeNode messageType = TypeParser.parseType(messageDescriptor); 569 570 List<FieldDescriptor> fields = messageDescriptor.getFields(); 571 HashMap<String, String> operationRequestFields = new HashMap<String, String>(); 572 BiMap<String, String> operationResponseFields = HashBiMap.create(); 573 OperationResponse.Builder operationResponse = null; 574 for (FieldDescriptor fd : fields) { 575 if (fd.getOptions().hasExtension(ExtendedOperationsProto.operationRequestField)) { 576 String orf = fd.getOptions().getExtension(ExtendedOperationsProto.operationRequestField); 577 operationRequestFields.put(orf, fd.getName()); 578 } 579 if (fd.getOptions().hasExtension(ExtendedOperationsProto.operationResponseField)) { 580 String orf = fd.getOptions().getExtension(ExtendedOperationsProto.operationResponseField); 581 operationResponseFields.put(orf, fd.getName()); 582 } 583 if (fd.getOptions().hasExtension(ExtendedOperationsProto.operationField)) { 584 OperationResponseMapping orm = 585 fd.getOptions().getExtension(ExtendedOperationsProto.operationField); 586 if (operationResponse == null) { 587 operationResponse = OperationResponse.builder(); 588 } 589 if (orm.equals(OperationResponseMapping.NAME)) { 590 operationResponse.setNameFieldName(fd.getName()); 591 } else if (orm.equals(OperationResponseMapping.STATUS)) { 592 operationResponse.setStatusFieldName(fd.getName()); 593 operationResponse.setStatusFieldTypeName(fd.toProto().getTypeName()); 594 } else if (orm.equals(OperationResponseMapping.ERROR_CODE)) { 595 operationResponse.setErrorCodeFieldName(fd.getName()); 596 } else if (orm.equals(OperationResponseMapping.ERROR_MESSAGE)) { 597 operationResponse.setErrorMessageFieldName(fd.getName()); 598 } 599 } 600 } 601 messages.put( 602 messageType.reference().fullName(), 603 Message.builder() 604 .setType(messageType) 605 .setName(messageName) 606 .setFullProtoName(messageDescriptor.getFullName()) 607 .setFields(parseFields(messageDescriptor, outputResourceReferencesSeen)) 608 .setOuterNestedTypes(outerNestedTypes) 609 .setOperationRequestFields(operationRequestFields) 610 .setOperationResponseFields(operationResponseFields) 611 .setOperationResponse(operationResponse != null ? operationResponse.build() : null) 612 .build()); 613 return messages; 614 } 615 isMapType(Descriptor messageDescriptor)616 private static boolean isMapType(Descriptor messageDescriptor) { 617 List<String> fieldNames = 618 messageDescriptor.getFields().stream().map(f -> f.getName()).collect(Collectors.toList()); 619 // Ends in "Entry" and has exactly two fields, named "key" and "value". 620 return messageDescriptor.getName().endsWith("Entry") 621 && fieldNames.size() == 2 622 && fieldNames.get(0).equals("key") 623 && fieldNames.get(1).equals("value"); 624 } 625 626 /** 627 * Populates ResourceName objects in Message POJOs. 628 * 629 * @param messageTypes A map of the message type name (as in the protobuf) to Message POJOs. 630 * @param resources A list of ResourceName POJOs. 631 * @return The updated messageTypes map. 632 */ updateResourceNamesInMessages( Map<String, Message> messageTypes, Collection<ResourceName> resources)633 public static Map<String, Message> updateResourceNamesInMessages( 634 Map<String, Message> messageTypes, Collection<ResourceName> resources) { 635 Map<String, Message> updatedMessages = new HashMap<>(messageTypes); 636 for (ResourceName resource : resources) { 637 if (!resource.hasParentMessageName()) { 638 continue; 639 } 640 String messageKey = resource.parentMessageName(); 641 Message messageToUpdate = updatedMessages.get(messageKey); 642 updatedMessages.put(messageKey, messageToUpdate.toBuilder().setResource(resource).build()); 643 } 644 return updatedMessages; 645 } 646 parseResourceNames(CodeGeneratorRequest request)647 public static Map<String, ResourceName> parseResourceNames(CodeGeneratorRequest request) { 648 String javaPackage = parseServiceJavaPackage(request); 649 Map<String, FileDescriptor> fileDescriptors = getFilesToGenerate(request); 650 Map<String, ResourceName> resourceNames = new HashMap<>(); 651 for (String fileToGenerate : request.getFileToGenerateList()) { 652 FileDescriptor fileDescriptor = 653 Preconditions.checkNotNull( 654 fileDescriptors.get(fileToGenerate), 655 "Missing file descriptor for [%s]", 656 fileToGenerate); 657 resourceNames.putAll(parseResourceNames(fileDescriptor, javaPackage)); 658 } 659 return resourceNames; 660 } 661 662 // Convenience wrapper for package-external unit tests. DO NOT ADD NEW FUNCTIONALITY HERE. parseResourceNames(FileDescriptor fileDescriptor)663 public static Map<String, ResourceName> parseResourceNames(FileDescriptor fileDescriptor) { 664 return parseResourceNames(fileDescriptor, TypeParser.getPackage(fileDescriptor)); 665 } 666 parseResourceNames( FileDescriptor fileDescriptor, String javaPackage)667 public static Map<String, ResourceName> parseResourceNames( 668 FileDescriptor fileDescriptor, String javaPackage) { 669 return ResourceNameParser.parseResourceNames(fileDescriptor, javaPackage); 670 } 671 672 @VisibleForTesting parseMethods( ServiceDescriptor serviceDescriptor, String servicePackage, Map<String, Message> messageTypes, Map<String, ResourceName> resourceNames, Optional<GapicServiceConfig> serviceConfigOpt, Set<ResourceName> outputArgResourceNames, Transport transport)673 static List<Method> parseMethods( 674 ServiceDescriptor serviceDescriptor, 675 String servicePackage, 676 Map<String, Message> messageTypes, 677 Map<String, ResourceName> resourceNames, 678 Optional<GapicServiceConfig> serviceConfigOpt, 679 Set<ResourceName> outputArgResourceNames, 680 Transport transport) { 681 List<Method> methods = new ArrayList<>(); 682 for (MethodDescriptor protoMethod : serviceDescriptor.getMethods()) { 683 // Parse the method. 684 TypeNode inputType = TypeParser.parseType(protoMethod.getInputType()); 685 Method.Builder methodBuilder = Method.builder(); 686 if (protoMethod.getFile().toProto().hasSourceCodeInfo()) { 687 SourceCodeInfoLocation protoMethodLocation = 688 SOURCE_CODE_INFO_PARSER.getLocation(protoMethod); 689 if (!Objects.isNull(protoMethodLocation) 690 && !Strings.isNullOrEmpty(protoMethodLocation.getLeadingComments())) { 691 methodBuilder.setDescription(protoMethodLocation.getLeadingComments()); 692 } 693 } 694 695 boolean isDeprecated = false; 696 if (protoMethod.getOptions().hasDeprecated()) { 697 isDeprecated = protoMethod.getOptions().getDeprecated(); 698 } 699 700 Message inputMessage = messageTypes.get(inputType.reference().fullName()); 701 Preconditions.checkNotNull( 702 inputMessage, String.format("No message found for %s", inputType.reference().fullName())); 703 HttpBindings httpBindings = HttpRuleParser.parse(protoMethod, inputMessage, messageTypes); 704 boolean isBatching = 705 !serviceConfigOpt.isPresent() 706 ? false 707 : serviceConfigOpt 708 .get() 709 .hasBatchingSetting( 710 /* protoPakkage */ protoMethod.getFile().getPackage(), 711 serviceDescriptor.getName(), 712 protoMethod.getName()); 713 714 boolean operationPollingMethod = 715 protoMethod.getOptions().hasExtension(ExtendedOperationsProto.operationPollingMethod) 716 ? protoMethod 717 .getOptions() 718 .getExtension(ExtendedOperationsProto.operationPollingMethod) 719 : false; 720 RoutingHeaderRule routingHeaderRule = 721 RoutingRuleParser.parse(protoMethod, inputMessage, messageTypes); 722 methods.add( 723 methodBuilder 724 .setName(protoMethod.getName()) 725 .setInputType(inputType) 726 .setOutputType(TypeParser.parseType(protoMethod.getOutputType())) 727 .setStream( 728 Method.toStream(protoMethod.isClientStreaming(), protoMethod.isServerStreaming())) 729 .setLro(parseLro(servicePackage, protoMethod, messageTypes)) 730 .setMethodSignatures( 731 MethodSignatureParser.parseMethodSignatures( 732 protoMethod, 733 servicePackage, 734 inputType, 735 messageTypes, 736 resourceNames, 737 outputArgResourceNames)) 738 .setHttpBindings(httpBindings) 739 .setRoutingHeaderRule(routingHeaderRule) 740 .setIsBatching(isBatching) 741 .setPageSizeFieldName(parsePageSizeFieldName(protoMethod, messageTypes, transport)) 742 .setIsDeprecated(isDeprecated) 743 .setOperationPollingMethod(operationPollingMethod) 744 .build()); 745 746 // Any input type that has a resource reference will need a resource name helper class. 747 for (Field field : inputMessage.fields()) { 748 if (field.hasResourceReference()) { 749 String resourceTypeString = field.resourceReference().resourceTypeString(); 750 ResourceName resourceName = null; 751 // Support older resource_references that specify only the final typename, e.g. FooBar 752 // versus example.com/FooBar. 753 if (resourceTypeString.indexOf(SLASH) < 0) { 754 Optional<String> actualResourceTypeNameOpt = 755 resourceNames.keySet().stream() 756 .filter(k -> k.substring(k.lastIndexOf(SLASH) + 1).equals(resourceTypeString)) 757 .findFirst(); 758 if (actualResourceTypeNameOpt.isPresent()) { 759 resourceName = resourceNames.get(actualResourceTypeNameOpt.get()); 760 } 761 } else { 762 resourceName = resourceNames.get(resourceTypeString); 763 } 764 765 if (ResourceNameConstants.WILDCARD_PATTERN.equals(resourceTypeString)) { 766 resourceName = WILDCARD_RESOURCE_NAME; 767 } else { 768 Preconditions.checkNotNull( 769 resourceName, 770 String.format( 771 "Resource name %s not found; parsing field %s in message %s in method %s", 772 resourceTypeString, field.name(), inputMessage.name(), protoMethod.getName())); 773 } 774 775 outputArgResourceNames.add(resourceName); 776 } 777 } 778 } 779 780 return methods; 781 } 782 783 @VisibleForTesting parseLro( String servicePackage, MethodDescriptor methodDescriptor, Map<String, Message> messageTypes)784 static LongrunningOperation parseLro( 785 String servicePackage, MethodDescriptor methodDescriptor, Map<String, Message> messageTypes) { 786 MethodOptions methodOptions = methodDescriptor.getOptions(); 787 788 TypeNode operationServiceStubType = null; 789 String responseTypeName = null; 790 String metadataTypeName = null; 791 792 if (methodOptions.hasExtension(OperationsProto.operationInfo)) { 793 OperationInfo lroInfo = 794 methodDescriptor.getOptions().getExtension(OperationsProto.operationInfo); 795 responseTypeName = lroInfo.getResponseType(); 796 metadataTypeName = lroInfo.getMetadataType(); 797 } 798 if (methodOptions.hasExtension(ExtendedOperationsProto.operationService)) { 799 // TODO: support full package name for operations_service annotation value 800 String opServiceName = methodOptions.getExtension(ExtendedOperationsProto.operationService); 801 operationServiceStubType = 802 TypeNode.withReference( 803 VaporReference.builder() 804 .setName(opServiceName + "Stub") 805 .setPakkage(servicePackage + ".stub") 806 .build()); 807 808 if (responseTypeName == null) { 809 responseTypeName = methodDescriptor.getOutputType().getFullName(); 810 } 811 if (metadataTypeName == null) { 812 metadataTypeName = methodDescriptor.getOutputType().getFullName(); 813 } 814 } 815 816 if (responseTypeName == null || metadataTypeName == null) { 817 return null; 818 } 819 820 Message responseMessage = null; 821 Message metadataMessage = null; 822 823 int lastDotIndex = responseTypeName.lastIndexOf('.'); 824 boolean isResponseTypeNameShortOnly = lastDotIndex < 0; 825 String responseTypeShortName = 826 lastDotIndex >= 0 ? responseTypeName.substring(lastDotIndex + 1) : responseTypeName; 827 828 lastDotIndex = metadataTypeName.lastIndexOf('.'); 829 boolean isMetadataTypeNameShortOnly = lastDotIndex < 0; 830 String metadataTypeShortName = 831 lastDotIndex >= 0 ? metadataTypeName.substring(lastDotIndex + 1) : metadataTypeName; 832 833 // The messageTypes map keys to the Java fully-qualified name. 834 for (Map.Entry<String, Message> messageEntry : messageTypes.entrySet()) { 835 String messageKey = messageEntry.getKey(); 836 int messageLastDotIndex = messageEntry.getKey().lastIndexOf('.'); 837 String messageShortName = 838 messageLastDotIndex >= 0 ? messageKey.substring(messageLastDotIndex + 1) : messageKey; 839 if (responseMessage == null) { 840 if (isResponseTypeNameShortOnly && responseTypeName.equals(messageShortName)) { 841 responseMessage = messageEntry.getValue(); 842 } else if (!isResponseTypeNameShortOnly && responseTypeShortName.equals(messageShortName)) { 843 // Ensure that the full proto name matches. 844 Message candidateMessage = messageEntry.getValue(); 845 if (candidateMessage.fullProtoName().equals(responseTypeName)) { 846 responseMessage = candidateMessage; 847 } 848 } 849 } 850 if (metadataMessage == null) { 851 if (isMetadataTypeNameShortOnly && metadataTypeName.equals(messageShortName)) { 852 metadataMessage = messageEntry.getValue(); 853 } else if (!isMetadataTypeNameShortOnly && metadataTypeShortName.equals(messageShortName)) { 854 // Ensure that the full proto name matches. 855 Message candidateMessage = messageEntry.getValue(); 856 if (candidateMessage.fullProtoName().equals(metadataTypeName)) { 857 metadataMessage = candidateMessage; 858 } 859 } 860 } 861 } 862 863 Preconditions.checkNotNull( 864 responseMessage, 865 String.format( 866 "LRO response message %s not found on method %s", 867 responseTypeName, methodDescriptor.getName())); 868 // TODO(miraleung): Check that the packages are equal if those strings are not empty. 869 Preconditions.checkNotNull( 870 metadataMessage, 871 String.format( 872 "LRO metadata message %s not found in method %s", 873 metadataTypeName, methodDescriptor.getName())); 874 875 return LongrunningOperation.builder() 876 .setResponseType(responseMessage.type()) 877 .setMetadataType(metadataMessage.type()) 878 .setOperationServiceStubType(operationServiceStubType) 879 .build(); 880 } 881 882 @VisibleForTesting parsePageSizeFieldName( MethodDescriptor methodDescriptor, Map<String, Message> messageTypes, Transport transport)883 static String parsePageSizeFieldName( 884 MethodDescriptor methodDescriptor, Map<String, Message> messageTypes, Transport transport) { 885 TypeNode inputMessageType = TypeParser.parseType(methodDescriptor.getInputType()); 886 TypeNode outputMessageType = TypeParser.parseType(methodDescriptor.getOutputType()); 887 Message inputMessage = messageTypes.get(inputMessageType.reference().fullName()); 888 Message outputMessage = messageTypes.get(outputMessageType.reference().fullName()); 889 890 // This should technically handle the absence of either of these fields (aip.dev/158), but we 891 // gate on their collective presence to ensure the generated surface is backawrds-compatible 892 // with monolith-gnerated libraries. 893 String pagedFieldName = null; 894 895 if (inputMessage != null 896 && inputMessage.fieldMap().containsKey("page_token") 897 && outputMessage != null 898 && outputMessage.fieldMap().containsKey("next_page_token")) { 899 // List of potential field names representing page size. 900 // page_size gets priority over max_results if both are present 901 List<String> fieldNames = new ArrayList<>(); 902 fieldNames.add("page_size"); 903 if (transport == Transport.REST) { 904 fieldNames.add("max_results"); 905 } 906 for (String fieldName : fieldNames) { 907 if (pagedFieldName == null && inputMessage.fieldMap().containsKey(fieldName)) { 908 pagedFieldName = fieldName; 909 } 910 } 911 } 912 return pagedFieldName; 913 } 914 915 @VisibleForTesting sanitizeDefaultHost(String rawDefaultHost)916 static String sanitizeDefaultHost(String rawDefaultHost) { 917 if (rawDefaultHost.contains(COLON)) { 918 // A port is already present, just return the existing string. 919 return rawDefaultHost; 920 } 921 return String.format("%s:%s", rawDefaultHost, DEFAULT_PORT); 922 } 923 parseFields( Descriptor messageDescriptor, Set<ResourceReference> outputResourceReferencesSeen)924 private static List<Field> parseFields( 925 Descriptor messageDescriptor, Set<ResourceReference> outputResourceReferencesSeen) { 926 List<FieldDescriptor> fields = new ArrayList<>(messageDescriptor.getFields()); 927 // Sort by ascending field index order. This is important for paged responses, where the first 928 // repeated type is taken. 929 fields.sort((f1, f2) -> f1.getIndex() - f2.getIndex()); 930 931 // Mirror protoc's name conflict resolution behavior for fields. 932 // If a singular field's name equals that of a repeated field with "Count" or "List" suffixed, 933 // append the protobuf's field number to both fields' names. 934 // See: 935 // https://github.com/protocolbuffers/protobuf/blob/9df42757f97da9f748a464deeda96427a8f7ade0/src/google/protobuf/compiler/java/java_context.cc#L60 936 Map<String, Integer> repeatedFieldNamesToNumber = 937 fields.stream() 938 .filter(f -> f.isRepeated()) 939 .collect(Collectors.toMap(f -> f.getName(), f -> f.getNumber())); 940 Set<Integer> fieldNumbersWithConflicts = new HashSet<>(); 941 for (FieldDescriptor field : fields) { 942 Set<String> conflictingRepeatedFieldNames = 943 repeatedFieldNamesToNumber.keySet().stream() 944 .filter( 945 n -> field.getName().equals(n + "_count") || field.getName().equals(n + "_list")) 946 .collect(Collectors.toSet()); 947 if (!conflictingRepeatedFieldNames.isEmpty()) { 948 fieldNumbersWithConflicts.addAll( 949 conflictingRepeatedFieldNames.stream() 950 .map(n -> repeatedFieldNamesToNumber.get(n)) 951 .collect(Collectors.toSet())); 952 fieldNumbersWithConflicts.add(field.getNumber()); 953 } 954 } 955 956 return fields.stream() 957 .map( 958 f -> 959 parseField( 960 f, 961 messageDescriptor, 962 fieldNumbersWithConflicts.contains(f.getNumber()), 963 outputResourceReferencesSeen)) 964 .collect(Collectors.toList()); 965 } 966 parseField( FieldDescriptor fieldDescriptor, Descriptor messageDescriptor, boolean hasFieldNameConflict, Set<ResourceReference> outputResourceReferencesSeen)967 private static Field parseField( 968 FieldDescriptor fieldDescriptor, 969 Descriptor messageDescriptor, 970 boolean hasFieldNameConflict, 971 Set<ResourceReference> outputResourceReferencesSeen) { 972 FieldOptions fieldOptions = fieldDescriptor.getOptions(); 973 MessageOptions messageOptions = messageDescriptor.getOptions(); 974 ResourceReference resourceReference = null; 975 if (fieldOptions.hasExtension(ResourceProto.resourceReference)) { 976 com.google.api.ResourceReference protoResourceReference = 977 fieldOptions.getExtension(ResourceProto.resourceReference); 978 // Assumes only one of type or child_type is set. 979 String typeString = protoResourceReference.getType(); 980 String childTypeString = protoResourceReference.getChildType(); 981 Preconditions.checkState( 982 !Strings.isNullOrEmpty(typeString) ^ !Strings.isNullOrEmpty(childTypeString), 983 String.format( 984 "Exactly one of type or child_type must be set for resource_reference in field %s", 985 fieldDescriptor.getName())); 986 boolean isChildType = !Strings.isNullOrEmpty(childTypeString); 987 resourceReference = 988 isChildType 989 ? ResourceReference.withChildType(childTypeString) 990 : ResourceReference.withType(typeString); 991 outputResourceReferencesSeen.add(resourceReference); 992 993 } else if (messageOptions.hasExtension(ResourceProto.resource)) { 994 ResourceDescriptor protoResource = messageOptions.getExtension(ResourceProto.resource); 995 // aip.dev/4231. 996 String resourceFieldNameValue = ResourceNameConstants.NAME_FIELD_NAME; 997 if (!Strings.isNullOrEmpty(protoResource.getNameField())) { 998 resourceFieldNameValue = protoResource.getNameField(); 999 } 1000 if (fieldDescriptor.getName().equals(resourceFieldNameValue)) { 1001 resourceReference = ResourceReference.withType(protoResource.getType()); 1002 } 1003 } 1004 1005 Field.Builder fieldBuilder = Field.builder(); 1006 if (fieldDescriptor.getFile().toProto().hasSourceCodeInfo()) { 1007 SourceCodeInfoLocation protoFieldLocation = 1008 SOURCE_CODE_INFO_PARSER.getLocation(fieldDescriptor); 1009 if (!Objects.isNull(protoFieldLocation) 1010 && !Strings.isNullOrEmpty(protoFieldLocation.getLeadingComments())) { 1011 fieldBuilder.setDescription(protoFieldLocation.getLeadingComments()); 1012 } 1013 } 1014 1015 // Mirror protoc's name conflict resolution behavior for fields. 1016 // For more context, trace hasFieldNameConflict back to where it gets passed in above. 1017 String actualFieldName = 1018 hasFieldNameConflict 1019 ? fieldDescriptor.getName() + fieldDescriptor.getNumber() 1020 : fieldDescriptor.getName(); 1021 1022 return fieldBuilder 1023 .setName(actualFieldName) 1024 .setOriginalName(fieldDescriptor.getName()) 1025 .setType(TypeParser.parseType(fieldDescriptor)) 1026 .setIsMessage(fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) 1027 .setIsEnum(fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.ENUM) 1028 .setIsContainedInOneof( 1029 fieldDescriptor.getContainingOneof() != null 1030 && !fieldDescriptor.getContainingOneof().isSynthetic()) 1031 .setIsProto3Optional( 1032 fieldDescriptor.getContainingOneof() != null 1033 && fieldDescriptor.getContainingOneof().isSynthetic()) 1034 .setIsRepeated(fieldDescriptor.isRepeated()) 1035 .setIsMap(fieldDescriptor.isMapField()) 1036 .setResourceReference(resourceReference) 1037 .build(); 1038 } 1039 getFilesToGenerate(CodeGeneratorRequest request)1040 private static Map<String, FileDescriptor> getFilesToGenerate(CodeGeneratorRequest request) { 1041 // Build the fileDescriptors map so that we can create the FDs for the filesToGenerate. 1042 Map<String, FileDescriptor> fileDescriptors = Maps.newHashMap(); 1043 for (FileDescriptorProto fileDescriptorProto : request.getProtoFileList()) { 1044 // Look up the imported files from previous file descriptors. It is sufficient to look at 1045 // only previous file descriptors because CodeGeneratorRequest guarantees that the files 1046 // are sorted in topological order. 1047 FileDescriptor[] deps = new FileDescriptor[fileDescriptorProto.getDependencyCount()]; 1048 for (int i = 0; i < fileDescriptorProto.getDependencyCount(); i++) { 1049 String name = fileDescriptorProto.getDependency(i); 1050 deps[i] = 1051 Preconditions.checkNotNull( 1052 fileDescriptors.get(name), "Missing file descriptor for [%s]", name); 1053 } 1054 1055 FileDescriptor fileDescriptor = null; 1056 try { 1057 fileDescriptor = FileDescriptor.buildFrom(fileDescriptorProto, deps); 1058 } catch (DescriptorValidationException e) { 1059 throw new GapicParserException(e.getMessage()); 1060 } 1061 1062 fileDescriptors.put(fileDescriptor.getName(), fileDescriptor); 1063 } 1064 return fileDescriptors; 1065 } 1066 parseServiceJavaPackage(CodeGeneratorRequest request)1067 private static String parseServiceJavaPackage(CodeGeneratorRequest request) { 1068 Map<String, Integer> javaPackageCount = new HashMap<>(); 1069 Map<String, FileDescriptor> fileDescriptors = getFilesToGenerate(request); 1070 for (String fileToGenerate : request.getFileToGenerateList()) { 1071 FileDescriptor fileDescriptor = 1072 Preconditions.checkNotNull( 1073 fileDescriptors.get(fileToGenerate), 1074 "Missing file descriptor for [%s]", 1075 fileToGenerate); 1076 1077 String javaPackage = fileDescriptor.getOptions().getJavaPackage(); 1078 if (Strings.isNullOrEmpty(javaPackage)) { 1079 continue; 1080 } 1081 if (javaPackageCount.containsKey(javaPackage)) { 1082 javaPackageCount.put(javaPackage, javaPackageCount.get(javaPackage) + 1); 1083 } else { 1084 javaPackageCount.put(javaPackage, 1); 1085 } 1086 } 1087 1088 // Filter out mixin packages. 1089 Map<String, Integer> processedJavaPackageCount = 1090 javaPackageCount.entrySet().stream() 1091 .filter(e -> !MIXIN_JAVA_PACKAGE_ALLOWLIST.contains(e.getKey())) 1092 .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); 1093 1094 // An empty map indicates that only mixin packages were present, which means that we're 1095 // generating a standalone client for a mixin. 1096 if (processedJavaPackageCount.isEmpty()) { 1097 processedJavaPackageCount = javaPackageCount; 1098 } 1099 1100 String finalJavaPackage = 1101 processedJavaPackageCount.entrySet().stream() 1102 .max(Map.Entry.comparingByValue()) 1103 .get() 1104 .getKey(); 1105 Preconditions.checkState( 1106 !Strings.isNullOrEmpty(finalJavaPackage), "No service Java package found"); 1107 return finalJavaPackage; 1108 } 1109 1110 /** 1111 * Retrieves the nested type name from a fully-qualified protobuf type name. Example: 1112 * google.ads.googleads.v3.resources.MutateJob.MutateJobMetadata > MutateJob.MutateJobMetadata. 1113 */ 1114 @VisibleForTesting parseNestedProtoTypeName(String fullyQualifiedName)1115 static String parseNestedProtoTypeName(String fullyQualifiedName) { 1116 if (!fullyQualifiedName.contains(DOT)) { 1117 return fullyQualifiedName; 1118 } 1119 // Find the first component in CapitalCamelCase. Assumes that proto package 1120 // components must be in all lowercase and type names are in CapitalCamelCase. 1121 String[] components = fullyQualifiedName.split("\\."); 1122 List<String> nestedTypeComponents = 1123 IntStream.range(0, components.length) 1124 .filter(i -> Character.isUpperCase(components[i].charAt(0))) 1125 .mapToObj(i -> components[i]) 1126 .collect(Collectors.toList()); 1127 return String.join(".", nestedTypeComponents); 1128 } 1129 } 1130