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