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.generator.engine.ast.TypeNode;
19 import com.google.api.generator.gapic.model.Field;
20 import com.google.api.generator.gapic.model.Message;
21 import com.google.api.generator.gapic.model.MethodArgument;
22 import com.google.api.generator.gapic.model.ResourceName;
23 import com.google.api.generator.gapic.model.ResourceReference;
24 import com.google.common.annotations.VisibleForTesting;
25 import com.google.common.base.Preconditions;
26 import com.google.common.base.Strings;
27 import com.google.common.collect.Lists;
28 import com.google.protobuf.Descriptors.MethodDescriptor;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.stream.Collectors;
36 
37 // TODO(miraleung): Add tests for this class. Currently exercised in integration tests.
38 public class MethodSignatureParser {
39   private static final String DOT = ".";
40   private static final String METHOD_SIGNATURE_DELIMITER = "\\s*,\\s*";
41 
42   /** Parses a list of method signature annotations out of an RPC. */
parseMethodSignatures( MethodDescriptor methodDescriptor, String servicePackage, TypeNode methodInputType, Map<String, Message> messageTypes, Map<String, ResourceName> resourceNames, Set<ResourceName> outputArgResourceNames)43   public static List<List<MethodArgument>> parseMethodSignatures(
44       MethodDescriptor methodDescriptor,
45       String servicePackage,
46       TypeNode methodInputType,
47       Map<String, Message> messageTypes,
48       Map<String, ResourceName> resourceNames,
49       Set<ResourceName> outputArgResourceNames) {
50     List<String> stringSigs =
51         methodDescriptor.getOptions().getExtension(ClientProto.methodSignature);
52 
53     List<List<MethodArgument>> signatures = new ArrayList<>();
54     if (stringSigs.isEmpty()) {
55       return signatures;
56     }
57 
58     Map<String, ResourceName> patternsToResourceNames =
59         ResourceParserHelpers.createPatternResourceNameMap(resourceNames);
60     Message inputMessage = messageTypes.get(methodInputType.reference().fullName());
61 
62     // Example from Expand in echo.proto:
63     // stringSigs: ["content,error", "content,error,info"].
64     for (String stringSig : stringSigs) {
65       if (Strings.isNullOrEmpty(stringSig)) {
66         signatures.add(Collections.emptyList());
67         continue;
68       }
69 
70       List<String> argumentNames = new ArrayList<>();
71       Map<String, List<MethodArgument>> argumentNameToOverloads = new HashMap<>();
72 
73       // stringSig.split: ["content", "error"].
74       for (String argumentName : stringSig.split(METHOD_SIGNATURE_DELIMITER)) {
75         // For resource names, this will be empty.
76         List<Field> argumentFieldPathAcc = new ArrayList<>();
77         // There should be more than one type returned only when we encounter a resource name.
78         Map<TypeNode, Field> argumentTypes =
79             parseTypeFromArgumentName(
80                 argumentName,
81                 servicePackage,
82                 inputMessage,
83                 messageTypes,
84                 resourceNames,
85                 patternsToResourceNames,
86                 argumentFieldPathAcc,
87                 outputArgResourceNames);
88         int dotLastIndex = argumentName.lastIndexOf(DOT);
89         String actualArgumentName =
90             dotLastIndex < 0 ? argumentName : argumentName.substring(dotLastIndex + 1);
91         argumentNames.add(actualArgumentName);
92 
93         argumentNameToOverloads.put(
94             actualArgumentName,
95             argumentTypes.entrySet().stream()
96                 .map(
97                     e ->
98                         MethodArgument.builder()
99                             .setName(actualArgumentName)
100                             .setType(e.getKey())
101                             .setField(e.getValue())
102                             .setIsResourceNameHelper(
103                                 argumentTypes.size() > 1 && !e.getKey().equals(TypeNode.STRING))
104                             .setNestedFields(argumentFieldPathAcc)
105                             .build())
106                 .collect(Collectors.toList()));
107       }
108       signatures.addAll(flattenMethodSignatureVariants(argumentNames, argumentNameToOverloads));
109     }
110 
111     // Make the method signature order deterministic, which helps with unit testing and per-version
112     // diffs.
113     List<List<MethodArgument>> sortedMethodSignatures =
114         signatures.stream()
115             .sorted(
116                 (s1, s2) -> {
117                   // Sort by number of arguments first.
118                   if (s1.size() != s2.size()) {
119                     return s1.size() - s2.size();
120                   }
121                   // Then by MethodSignature properties.
122                   for (int i = 0; i < s1.size(); i++) {
123                     int compareVal = s1.get(i).compareTo(s2.get(i));
124                     if (compareVal != 0) {
125                       return compareVal;
126                     }
127                   }
128                   return 0;
129                 })
130             .collect(Collectors.toList());
131 
132     return sortedMethodSignatures;
133   }
134 
135   @VisibleForTesting
flattenMethodSignatureVariants( List<String> argumentNames, Map<String, List<MethodArgument>> argumentNameToOverloads)136   static List<List<MethodArgument>> flattenMethodSignatureVariants(
137       List<String> argumentNames, Map<String, List<MethodArgument>> argumentNameToOverloads) {
138     Preconditions.checkState(
139         argumentNames.size() == argumentNameToOverloads.size(),
140         String.format(
141             "Cardinality of argument names %s do not match that of overloaded types %s",
142             argumentNames, argumentNameToOverloads));
143     for (String name : argumentNames) {
144       Preconditions.checkNotNull(
145           argumentNameToOverloads.get(name),
146           String.format("No corresponding overload types found for argument %s", name));
147     }
148     return flattenMethodSignatureVariants(argumentNames, argumentNameToOverloads, 0);
149   }
150 
flattenMethodSignatureVariants( List<String> argumentNames, Map<String, List<MethodArgument>> argumentNameToOverloads, int depth)151   private static List<List<MethodArgument>> flattenMethodSignatureVariants(
152       List<String> argumentNames,
153       Map<String, List<MethodArgument>> argumentNameToOverloads,
154       int depth) {
155     List<List<MethodArgument>> methodArgs = new ArrayList<>();
156     if (depth >= argumentNames.size() - 1) {
157       for (MethodArgument methodArg : argumentNameToOverloads.get(argumentNames.get(depth))) {
158         methodArgs.add(Lists.newArrayList(methodArg));
159       }
160       return methodArgs;
161     }
162 
163     List<List<MethodArgument>> subsequentArgs =
164         flattenMethodSignatureVariants(argumentNames, argumentNameToOverloads, depth + 1);
165     for (MethodArgument methodArg : argumentNameToOverloads.get(argumentNames.get(depth))) {
166       for (List<MethodArgument> subsequentArg : subsequentArgs) {
167         // Use a new list to avoid appending all subsequent elements (in upcoming loop iterations)
168         // to the same list.
169         List<MethodArgument> appendedArgs = new ArrayList<>(subsequentArg);
170         appendedArgs.add(0, methodArg);
171         methodArgs.add(appendedArgs);
172       }
173     }
174     return methodArgs;
175   }
176 
parseTypeFromArgumentName( String argumentName, String servicePackage, Message inputMessage, Map<String, Message> messageTypes, Map<String, ResourceName> resourceNames, Map<String, ResourceName> patternsToResourceNames, List<Field> argumentFieldPathAcc, Set<ResourceName> outputArgResourceNames)177   private static Map<TypeNode, Field> parseTypeFromArgumentName(
178       String argumentName,
179       String servicePackage,
180       Message inputMessage,
181       Map<String, Message> messageTypes,
182       Map<String, ResourceName> resourceNames,
183       Map<String, ResourceName> patternsToResourceNames,
184       List<Field> argumentFieldPathAcc,
185       Set<ResourceName> outputArgResourceNames) {
186 
187     Map<TypeNode, Field> typeToField = new HashMap<>();
188     int dotIndex = argumentName.indexOf(DOT);
189     if (dotIndex < 1) {
190       Field field = inputMessage.fieldMap().get(argumentName);
191       Preconditions.checkNotNull(
192           field,
193           String.format(
194               "Field %s not found from input message %s values %s",
195               argumentName, inputMessage.name(), inputMessage.fieldMap().keySet()));
196       if (!field.hasResourceReference()) {
197         typeToField.put(field.type(), field);
198         return typeToField;
199       }
200 
201       // Parse the resource name tyeps.
202       List<ResourceName> resourceNameArgs =
203           ResourceReferenceParser.parseResourceNames(
204               field.resourceReference(),
205               servicePackage,
206               field.description(),
207               resourceNames,
208               patternsToResourceNames);
209       outputArgResourceNames.addAll(resourceNameArgs);
210       typeToField.put(TypeNode.STRING, field);
211       typeToField.putAll(
212           resourceNameArgs.stream()
213               .collect(
214                   Collectors.toMap(
215                       r -> r.type(),
216                       r ->
217                           // Contruct a new field using the parent resource.
218                           field
219                               .toBuilder()
220                               .setResourceReference(
221                                   ResourceReference.withType(r.resourceTypeString()))
222                               .build())));
223       // Only resource name helpers should have more than one entry.
224       if (typeToField.size() > 1) {
225         typeToField.entrySet().stream()
226             .forEach(
227                 e -> {
228                   // Skip string-only variants or ResourceName generics.
229                   if (e.getKey().equals(TypeNode.STRING)
230                       || e.getKey().reference().name().equals("ResourceName")) {
231                     return;
232                   }
233                   String resourceJavaTypeName = e.getKey().reference().name();
234                   String resourceTypeName = e.getValue().resourceReference().resourceTypeString();
235                   int indexOfSlash = resourceTypeName.indexOf("/");
236                   // We assume that the corresponding Java resource name helper type (i.e. the key)
237                   // ends in *Name. Check that it matches the expeced resource name type.
238                   Preconditions.checkState(
239                       resourceJavaTypeName
240                           .substring(0, resourceJavaTypeName.length() - 4)
241                           .equals(resourceTypeName.substring(indexOfSlash + 1)),
242                       String.format(
243                           "Resource Java type %s does not correspond to proto type %s",
244                           resourceJavaTypeName, resourceTypeName));
245                 });
246       }
247       return typeToField;
248     }
249 
250     Preconditions.checkState(
251         dotIndex < argumentName.length() - 1,
252         String.format(
253             "Invalid argument name found: dot cannot be at the end of name %s", argumentName));
254     String firstFieldName = argumentName.substring(0, dotIndex);
255     String remainingArgumentName = argumentName.substring(dotIndex + 1);
256 
257     // Must be a sub-message for a type's subfield to be valid.
258     Field firstField = inputMessage.fieldMap().get(firstFieldName);
259 
260     // Validate the field into which we're descending.
261     Preconditions.checkState(
262         !firstField.isRepeated(),
263         String.format("Cannot descend into repeated field %s", firstField.name()));
264 
265     TypeNode firstFieldType = firstField.type();
266     Preconditions.checkState(
267         TypeNode.isReferenceType(firstFieldType) && !firstFieldType.equals(TypeNode.STRING),
268         String.format("Field reference on %s cannot be a primitive type", firstFieldName));
269 
270     String firstFieldTypeName = firstFieldType.reference().fullName();
271     Message firstFieldMessage = messageTypes.get(firstFieldTypeName);
272     Preconditions.checkNotNull(
273         firstFieldMessage,
274         String.format(
275             "Message type %s for field reference %s invalid", firstFieldTypeName, firstFieldName));
276 
277     argumentFieldPathAcc.add(firstField);
278     return parseTypeFromArgumentName(
279         remainingArgumentName,
280         servicePackage,
281         firstFieldMessage,
282         messageTypes,
283         resourceNames,
284         patternsToResourceNames,
285         argumentFieldPathAcc,
286         outputArgResourceNames);
287   }
288 }
289