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