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.composer.defaultvalue;
16 
17 import com.google.api.generator.engine.ast.AnonymousClassExpr;
18 import com.google.api.generator.engine.ast.AssignmentExpr;
19 import com.google.api.generator.engine.ast.ConcreteReference;
20 import com.google.api.generator.engine.ast.Expr;
21 import com.google.api.generator.engine.ast.ExprStatement;
22 import com.google.api.generator.engine.ast.MethodDefinition;
23 import com.google.api.generator.engine.ast.MethodInvocationExpr;
24 import com.google.api.generator.engine.ast.NewObjectExpr;
25 import com.google.api.generator.engine.ast.PrimitiveValue;
26 import com.google.api.generator.engine.ast.ScopeNode;
27 import com.google.api.generator.engine.ast.StringObjectValue;
28 import com.google.api.generator.engine.ast.TypeNode;
29 import com.google.api.generator.engine.ast.ValueExpr;
30 import com.google.api.generator.engine.ast.VaporReference;
31 import com.google.api.generator.engine.ast.Variable;
32 import com.google.api.generator.engine.ast.VariableExpr;
33 import com.google.api.generator.engine.lexicon.Keyword;
34 import com.google.api.generator.gapic.composer.resourcename.ResourceNameTokenizer;
35 import com.google.api.generator.gapic.model.Field;
36 import com.google.api.generator.gapic.model.HttpBindings;
37 import com.google.api.generator.gapic.model.Message;
38 import com.google.api.generator.gapic.model.MethodArgument;
39 import com.google.api.generator.gapic.model.ResourceName;
40 import com.google.api.generator.gapic.utils.JavaStyle;
41 import com.google.api.generator.gapic.utils.ResourceNameConstants;
42 import com.google.api.generator.gapic.utils.ResourceReferenceUtils;
43 import com.google.common.annotations.VisibleForTesting;
44 import com.google.common.base.Preconditions;
45 import com.google.longrunning.Operation;
46 import com.google.protobuf.Any;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collections;
50 import java.util.HashMap;
51 import java.util.HashSet;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Optional;
55 import java.util.regex.Pattern;
56 import java.util.stream.Collectors;
57 
58 public class DefaultValueComposer {
59   private static final TypeNode OPERATION_TYPE =
60       TypeNode.withReference(ConcreteReference.withClazz(Operation.class));
61   private static final TypeNode ANY_TYPE =
62       TypeNode.withReference(ConcreteReference.withClazz(Any.class));
63 
64   private static final Pattern REPLACER_PATTERN = Pattern.compile("(\\w*)(\\w/|-\\d+/)\\*");
65 
createMethodArgValue( MethodArgument methodArg, Map<String, ResourceName> resourceNames, Map<String, Message> messageTypes, Map<String, String> valuePatterns, HttpBindings bindings)66   public static Expr createMethodArgValue(
67       MethodArgument methodArg,
68       Map<String, ResourceName> resourceNames,
69       Map<String, Message> messageTypes,
70       Map<String, String> valuePatterns,
71       HttpBindings bindings) {
72     if (methodArg.isResourceNameHelper()) {
73       Preconditions.checkState(
74           methodArg.field().hasResourceReference(),
75           String.format(
76               "No corresponding resource reference for argument %s found on field %s %s",
77               methodArg.name(), methodArg.field().type(), methodArg.field().name()));
78       ResourceName resourceName =
79           resourceNames.get(methodArg.field().resourceReference().resourceTypeString());
80       Preconditions.checkNotNull(
81           resourceName,
82           String.format(
83               "No resource name found for reference %s",
84               methodArg.field().resourceReference().resourceTypeString()));
85       Expr defValue =
86           createResourceHelperValue(
87               resourceName,
88               methodArg.field().resourceReference().isChildType(),
89               resourceNames.values().stream().collect(Collectors.toList()),
90               methodArg.field().name(),
91               bindings);
92 
93       if (!methodArg.isResourceNameHelper() && methodArg.field().hasResourceReference()) {
94         defValue =
95             MethodInvocationExpr.builder()
96                 .setExprReferenceExpr(defValue)
97                 .setMethodName("toString")
98                 .setReturnType(TypeNode.STRING)
99                 .build();
100       }
101       return defValue;
102     }
103 
104     if (methodArg.type().equals(methodArg.field().type())) {
105       return createValue(methodArg.field(), false, resourceNames, messageTypes, valuePatterns);
106     }
107 
108     return createValue(Field.builder().setName(methodArg.name()).setType(methodArg.type()).build());
109   }
110 
createValue(Field field)111   public static Expr createValue(Field field) {
112     return createValue(
113         field, false, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap());
114   }
115 
createValue( Field field, boolean useExplicitInitTypeInGenerics, Map<String, ResourceName> resourceNames, Map<String, Message> messageTypes, Map<String, String> valuePatterns)116   public static Expr createValue(
117       Field field,
118       boolean useExplicitInitTypeInGenerics,
119       Map<String, ResourceName> resourceNames,
120       Map<String, Message> messageTypes,
121       Map<String, String> valuePatterns) {
122     if (field.isRepeated()) {
123       ConcreteReference.Builder refBuilder =
124           ConcreteReference.builder().setClazz(field.isMap() ? HashMap.class : ArrayList.class);
125       if (useExplicitInitTypeInGenerics) {
126         if (field.isMap()) {
127           refBuilder = refBuilder.setGenerics(field.type().reference().generics().subList(0, 2));
128         } else {
129           refBuilder = refBuilder.setGenerics(field.type().reference().generics().get(0));
130         }
131       }
132 
133       TypeNode newType = TypeNode.withReference(refBuilder.build());
134       return NewObjectExpr.builder().setType(newType).setIsGeneric(true).build();
135     }
136 
137     if (field.isEnum()) {
138       return MethodInvocationExpr.builder()
139           .setStaticReferenceType(field.type())
140           .setMethodName("forNumber")
141           .setArguments(
142               ValueExpr.withValue(
143                   PrimitiveValue.builder().setType(TypeNode.INT).setValue("0").build()))
144           .setReturnType(field.type())
145           .build();
146     }
147 
148     if (field.isMessage()) {
149       String nestedFieldName = field.name();
150       Map<String, String> nestedValuePatterns = new HashMap<>();
151       for (Map.Entry<String, String> entry : valuePatterns.entrySet()) {
152         String lowerCamelNestedFieldName = JavaStyle.toLowerCamelCase(nestedFieldName);
153         lowerCamelNestedFieldName = Keyword.unescapeKeyword(lowerCamelNestedFieldName);
154         if (entry.getKey().startsWith(lowerCamelNestedFieldName + '.')) {
155           nestedValuePatterns.put(
156               entry.getKey().substring(lowerCamelNestedFieldName.length() + 1), entry.getValue());
157         }
158       }
159 
160       if (!nestedValuePatterns.isEmpty()) {
161         Message nestedMessage = messageTypes.get(field.type().reference().fullName());
162         if (nestedMessage != null) {
163           return createSimpleMessageBuilderValue(
164               nestedMessage, resourceNames, messageTypes, nestedValuePatterns, null);
165         }
166       }
167 
168       MethodInvocationExpr newBuilderExpr =
169           MethodInvocationExpr.builder()
170               .setStaticReferenceType(field.type())
171               .setMethodName("newBuilder")
172               .build();
173       if (field.type().equals(TypeNode.VALUE)) {
174         newBuilderExpr =
175             MethodInvocationExpr.builder()
176                 .setExprReferenceExpr(newBuilderExpr)
177                 .setMethodName("setBoolValue")
178                 .setArguments(
179                     ValueExpr.withValue(
180                         PrimitiveValue.builder()
181                             .setType(TypeNode.BOOLEAN)
182                             .setValue("true")
183                             .build()))
184                 .build();
185       }
186 
187       return MethodInvocationExpr.builder()
188           .setExprReferenceExpr(newBuilderExpr)
189           .setMethodName("build")
190           .setReturnType(field.type())
191           .build();
192     }
193 
194     if (field.type().equals(TypeNode.STRING)) {
195       String javaFieldName = JavaStyle.toLowerCamelCase(field.name());
196       return ValueExpr.withValue(
197           StringObjectValue.withValue(
198               constructValueMatchingPattern(javaFieldName, valuePatterns.get(javaFieldName))));
199     }
200 
201     if (TypeNode.isNumericType(field.type())) {
202       return ValueExpr.withValue(
203           PrimitiveValue.builder()
204               .setType(field.type())
205               .setValue(String.format("%s", field.name().hashCode()))
206               .build());
207     }
208 
209     if (field.type().equals(TypeNode.BOOLEAN)) {
210       return ValueExpr.withValue(
211           PrimitiveValue.builder().setType(field.type()).setValue("true").build());
212     }
213 
214     if (field.type().equals(TypeNode.BYTESTRING)) {
215       return VariableExpr.builder()
216           .setStaticReferenceType(TypeNode.BYTESTRING)
217           .setVariable(Variable.builder().setName("EMPTY").setType(TypeNode.BYTESTRING).build())
218           .build();
219     }
220 
221     throw new UnsupportedOperationException(
222         String.format(
223             "Default value for field %s with type %s not implemented yet.",
224             field.name(), field.type()));
225   }
226 
createResourceHelperValue( ResourceName resourceName, boolean isChildType, List<ResourceName> resnames, String fieldOrMessageName, HttpBindings bindings)227   public static Expr createResourceHelperValue(
228       ResourceName resourceName,
229       boolean isChildType,
230       List<ResourceName> resnames,
231       String fieldOrMessageName,
232       HttpBindings bindings) {
233     return createResourceHelperValue(
234         resourceName, isChildType, resnames, fieldOrMessageName, true, bindings);
235   }
236 
findParentResource( ResourceName childResource, List<ResourceName> resourceNames)237   private static Optional<ResourceName> findParentResource(
238       ResourceName childResource, List<ResourceName> resourceNames) {
239     Map<String, ResourceName> patternToResourceName = new HashMap<>();
240 
241     for (ResourceName resourceName : resourceNames) {
242       for (String parentPattern : resourceName.patterns()) {
243         patternToResourceName.put(parentPattern, resourceName);
244       }
245     }
246 
247     for (String childPattern : childResource.patterns()) {
248       Optional<String> parentPattern = ResourceReferenceUtils.parseParentPattern(childPattern);
249       if (parentPattern.isPresent() && patternToResourceName.containsKey(parentPattern.get())) {
250         return Optional.of(patternToResourceName.get(parentPattern.get()));
251       }
252     }
253 
254     return Optional.empty();
255   }
256 
257   @VisibleForTesting
createResourceHelperValue( ResourceName resourceName, boolean isChildType, List<ResourceName> resnames, String fieldOrMessageName, boolean allowAnonResourceNameClass, HttpBindings bindings)258   static Expr createResourceHelperValue(
259       ResourceName resourceName,
260       boolean isChildType,
261       List<ResourceName> resnames,
262       String fieldOrMessageName,
263       boolean allowAnonResourceNameClass,
264       HttpBindings bindings) {
265 
266     if (isChildType) {
267       resourceName = findParentResource(resourceName, resnames).orElse(resourceName);
268     }
269 
270     if (resourceName.isOnlyWildcard()) {
271       List<ResourceName> unexaminedResnames = new ArrayList<>(resnames);
272       for (ResourceName resname : resnames) {
273         unexaminedResnames.remove(resname);
274         if (!resname.isOnlyWildcard()
275             && (bindings == null || resname.getMatchingPattern(bindings) != null)) {
276           return createResourceHelperValue(
277               resname, false, unexaminedResnames, fieldOrMessageName, null);
278         }
279       }
280 
281       return allowAnonResourceNameClass
282           ? createAnonymousResourceNameClassValue(fieldOrMessageName, bindings)
283           : ValueExpr.withValue(
284               StringObjectValue.withValue(
285                   String.format("%s%s", fieldOrMessageName, fieldOrMessageName.hashCode())));
286     }
287 
288     // The cost tradeoffs of new ctors versus distinct() don't really matter here, since this list
289     // will usually have a very small number of elements.
290     List<String> patterns = new ArrayList<>(new HashSet<>(resourceName.patterns()));
291     boolean containsOnlyDeletedTopic =
292         patterns.size() == 1 && patterns.get(0).equals(ResourceNameConstants.DELETED_TOPIC_LITERAL);
293     String ofMethodName = "of";
294     List<String> patternPlaceholderTokens = new ArrayList<>();
295 
296     if (containsOnlyDeletedTopic) {
297       ofMethodName = "ofDeletedTopic";
298     } else {
299       String matchingPattern = null;
300       if (bindings != null) {
301         matchingPattern = resourceName.getMatchingPattern(bindings);
302       }
303       if (matchingPattern == null) {
304         for (String pattern : resourceName.patterns()) {
305           if (pattern.equals(ResourceNameConstants.WILDCARD_PATTERN)
306               || pattern.equals(ResourceNameConstants.DELETED_TOPIC_LITERAL)) {
307             continue;
308           }
309           matchingPattern = pattern;
310           break;
311         }
312       }
313       patternPlaceholderTokens.addAll(
314           ResourceNameTokenizer.parseTokenHierarchy(Arrays.asList(matchingPattern)).get(0));
315     }
316 
317     boolean hasOnePattern = resourceName.patterns().size() == 1;
318     if (!hasOnePattern) {
319       ofMethodName =
320           String.format(
321               "of%sName",
322               String.join(
323                   "",
324                   patternPlaceholderTokens.stream()
325                       .map(s -> JavaStyle.toUpperCamelCase(s))
326                       .collect(Collectors.toList())));
327     }
328 
329     TypeNode resourceNameJavaType = resourceName.type();
330     List<Expr> argExprs =
331         patternPlaceholderTokens.stream()
332             .map(
333                 s ->
334                     ValueExpr.withValue(
335                         StringObjectValue.withValue(String.format("[%s]", s.toUpperCase()))))
336             .collect(Collectors.toList());
337     return MethodInvocationExpr.builder()
338         .setStaticReferenceType(resourceNameJavaType)
339         .setMethodName(ofMethodName)
340         .setArguments(argExprs)
341         .setReturnType(resourceNameJavaType)
342         .build();
343   }
344 
createSimpleMessageBuilderValue( Message message, Map<String, ResourceName> resourceNames, Map<String, Message> messageTypes, HttpBindings bindings)345   public static Expr createSimpleMessageBuilderValue(
346       Message message,
347       Map<String, ResourceName> resourceNames,
348       Map<String, Message> messageTypes,
349       HttpBindings bindings) {
350     return createSimpleMessageBuilderValue(
351         message, resourceNames, messageTypes, Collections.emptyMap(), bindings);
352   }
353 
createSimpleMessageBuilderValue( Message message, Map<String, ResourceName> resourceNames, Map<String, Message> messageTypes, Map<String, String> valuePatterns, HttpBindings bindings)354   public static Expr createSimpleMessageBuilderValue(
355       Message message,
356       Map<String, ResourceName> resourceNames,
357       Map<String, Message> messageTypes,
358       Map<String, String> valuePatterns,
359       HttpBindings bindings) {
360     MethodInvocationExpr builderExpr =
361         MethodInvocationExpr.builder()
362             .setStaticReferenceType(message.type())
363             .setMethodName("newBuilder")
364             .build();
365 
366     for (Field field : message.fields()) {
367       if (field.isContainedInOneof() // Avoid colliding fields.
368           || ((field.isMessage()
369                   || (field.isEnum()
370                       && message.operationResponse() == null)) // Avoid importing unparsed messages.
371               && !field.isRepeated()
372               && !messageTypes.containsKey(field.type().reference().fullName()))) {
373         continue;
374       }
375       String setterMethodNamePattern = "set%s";
376       if (field.isRepeated()) {
377         setterMethodNamePattern = field.isMap() ? "putAll%s" : "addAll%s";
378       }
379       Expr defaultExpr = null;
380       if (field.hasResourceReference()
381           && resourceNames.get(field.resourceReference().resourceTypeString()) != null) {
382         defaultExpr =
383             createResourceHelperValue(
384                 resourceNames.get(field.resourceReference().resourceTypeString()),
385                 field.resourceReference().isChildType(),
386                 resourceNames.values().stream().collect(Collectors.toList()),
387                 message.name(),
388                 /* allowAnonResourceNameClass = */ false,
389                 bindings);
390         defaultExpr =
391             MethodInvocationExpr.builder()
392                 .setExprReferenceExpr(defaultExpr)
393                 .setMethodName("toString")
394                 .setReturnType(TypeNode.STRING)
395                 .build();
396       } else {
397         if (message.operationResponse() != null) {
398           if (field.name().equals(message.operationResponse().statusFieldName())) {
399             String statusTypeName = message.operationResponse().statusFieldTypeName();
400             String statusClassName = statusTypeName.substring(statusTypeName.lastIndexOf('.') + 1);
401 
402             TypeNode statusType =
403                 TypeNode.withReference(
404                     VaporReference.builder()
405                         .setName(statusClassName)
406                         .setPakkage(message.type().reference().fullName())
407                         .setIsStaticImport(false)
408                         .build());
409             defaultExpr =
410                 VariableExpr.builder()
411                     .setVariable(Variable.builder().setName("DONE").setType(statusType).build())
412                     .setStaticReferenceType(statusType)
413                     .build();
414 
415           } else if (field.name().equals(message.operationResponse().errorCodeFieldName())) {
416             defaultExpr =
417                 ValueExpr.withValue(
418                     PrimitiveValue.builder().setType(field.type()).setValue("0").build());
419           }
420         }
421 
422         if (defaultExpr == null) {
423           defaultExpr = createValue(field, true, resourceNames, messageTypes, valuePatterns);
424         }
425       }
426       builderExpr =
427           MethodInvocationExpr.builder()
428               .setExprReferenceExpr(builderExpr)
429               .setMethodName(
430                   String.format(setterMethodNamePattern, JavaStyle.toUpperCamelCase(field.name())))
431               .setArguments(defaultExpr)
432               .build();
433     }
434 
435     return MethodInvocationExpr.builder()
436         .setExprReferenceExpr(builderExpr)
437         .setMethodName("build")
438         .setReturnType(message.type())
439         .build();
440   }
441 
createSimpleOperationBuilderValue(String name, VariableExpr responseExpr)442   public static Expr createSimpleOperationBuilderValue(String name, VariableExpr responseExpr) {
443     Expr operationExpr =
444         MethodInvocationExpr.builder()
445             .setStaticReferenceType(OPERATION_TYPE)
446             .setMethodName("newBuilder")
447             .build();
448     operationExpr =
449         MethodInvocationExpr.builder()
450             .setExprReferenceExpr(operationExpr)
451             .setMethodName("setName")
452             .setArguments(ValueExpr.withValue(StringObjectValue.withValue(name)))
453             .build();
454     operationExpr =
455         MethodInvocationExpr.builder()
456             .setExprReferenceExpr(operationExpr)
457             .setMethodName("setDone")
458             .setArguments(
459                 ValueExpr.withValue(
460                     PrimitiveValue.builder().setType(TypeNode.BOOLEAN).setValue("true").build()))
461             .build();
462     operationExpr =
463         MethodInvocationExpr.builder()
464             .setExprReferenceExpr(operationExpr)
465             .setMethodName("setResponse")
466             .setArguments(
467                 MethodInvocationExpr.builder()
468                     .setStaticReferenceType(ANY_TYPE)
469                     .setMethodName("pack")
470                     .setArguments(responseExpr)
471                     .build())
472             .build();
473     return MethodInvocationExpr.builder()
474         .setExprReferenceExpr(operationExpr)
475         .setMethodName("build")
476         .setReturnType(OPERATION_TYPE)
477         .build();
478   }
479 
createSimplePagedResponseValue( TypeNode responseType, String repeatedFieldName, Expr responseElementVarExpr, boolean isMap)480   public static Expr createSimplePagedResponseValue(
481       TypeNode responseType, String repeatedFieldName, Expr responseElementVarExpr, boolean isMap) {
482     // Code for paginated maps:
483     // AggregatedMessageList.newBuilder()
484     //     .setNextPageToken("")
485     //     .putAllItems(Collections.singletonMap("items", responsesElement))
486     //     .build();
487     //
488     // Code for paginated arrays:
489     // MessageList expectedResponse =
490     //     AddressList.newBuilder()
491     //         .setNextPageToken("")
492     //         .addAllItems(Arrays.asList(responsesElement))
493     //         .build();
494     Expr pagedResponseExpr =
495         MethodInvocationExpr.builder()
496             .setStaticReferenceType(responseType)
497             .setMethodName("newBuilder")
498             .build();
499     pagedResponseExpr =
500         MethodInvocationExpr.builder()
501             .setExprReferenceExpr(pagedResponseExpr)
502             .setMethodName("setNextPageToken")
503             .setArguments(ValueExpr.withValue(StringObjectValue.withValue("")))
504             .build();
505     if (isMap) {
506       pagedResponseExpr =
507           MethodInvocationExpr.builder()
508               .setExprReferenceExpr(pagedResponseExpr)
509               .setMethodName(
510                   String.format("putAll%s", JavaStyle.toUpperCamelCase(repeatedFieldName)))
511               .setArguments(
512                   MethodInvocationExpr.builder()
513                       .setStaticReferenceType(
514                           TypeNode.withReference(ConcreteReference.withClazz(Collections.class)))
515                       .setMethodName("singletonMap")
516                       .setArguments(
517                           ValueExpr.withValue(
518                               StringObjectValue.withValue(
519                                   JavaStyle.toLowerCamelCase(repeatedFieldName))),
520                           responseElementVarExpr)
521                       .build())
522               .build();
523     } else {
524       pagedResponseExpr =
525           MethodInvocationExpr.builder()
526               .setExprReferenceExpr(pagedResponseExpr)
527               .setMethodName(
528                   String.format("addAll%s", JavaStyle.toUpperCamelCase(repeatedFieldName)))
529               .setArguments(
530                   MethodInvocationExpr.builder()
531                       .setStaticReferenceType(
532                           TypeNode.withReference(ConcreteReference.withClazz(Arrays.class)))
533                       .setMethodName("asList")
534                       .setArguments(responseElementVarExpr)
535                       .build())
536               .build();
537     }
538     return MethodInvocationExpr.builder()
539         .setExprReferenceExpr(pagedResponseExpr)
540         .setMethodName("build")
541         .setReturnType(responseType)
542         .build();
543   }
544 
545   @VisibleForTesting
createAnonymousResourceNameClassValue( String fieldOrMessageName, HttpBindings matchedBindings)546   static AnonymousClassExpr createAnonymousResourceNameClassValue(
547       String fieldOrMessageName, HttpBindings matchedBindings) {
548     TypeNode stringMapType =
549         TypeNode.withReference(
550             ConcreteReference.builder()
551                 .setClazz(Map.class)
552                 .setGenerics(
553                     Arrays.asList(
554                         ConcreteReference.withClazz(String.class),
555                         ConcreteReference.withClazz(String.class)))
556                 .build());
557 
558     // Method code:
559     // @Override
560     // public Map<String, String> getFieldValuesMap() {
561     //   Map<String, String> fieldValuesMap = new HashMap<>();
562     //   fieldValuesMap.put("resource", "resource-12345");
563     //   return fieldValuesMap;
564     // }
565 
566     String toStringValue = String.format("%s%s", fieldOrMessageName, fieldOrMessageName.hashCode());
567     if (matchedBindings != null) {
568       Map<String, String> valuePatterns = matchedBindings.getPathParametersValuePatterns();
569       toStringValue =
570           constructValueMatchingPattern(fieldOrMessageName, valuePatterns.get(fieldOrMessageName));
571     }
572 
573     VariableExpr fieldValuesMapVarExpr =
574         VariableExpr.withVariable(
575             Variable.builder().setType(stringMapType).setName("fieldValuesMap").build());
576     StringObjectValue fieldOrMessageStringValue = StringObjectValue.withValue(toStringValue);
577 
578     List<Expr> bodyExprs =
579         Arrays.asList(
580             AssignmentExpr.builder()
581                 .setVariableExpr(fieldValuesMapVarExpr.toBuilder().setIsDecl(true).build())
582                 .setValueExpr(
583                     NewObjectExpr.builder()
584                         .setType(TypeNode.withReference(ConcreteReference.withClazz(HashMap.class)))
585                         .setIsGeneric(true)
586                         .build())
587                 .build(),
588             MethodInvocationExpr.builder()
589                 .setExprReferenceExpr(fieldValuesMapVarExpr)
590                 .setMethodName("put")
591                 .setArguments(
592                     ValueExpr.withValue(StringObjectValue.withValue(fieldOrMessageName)),
593                     ValueExpr.withValue(fieldOrMessageStringValue))
594                 .build());
595 
596     MethodDefinition getFieldValuesMapMethod =
597         MethodDefinition.builder()
598             .setIsOverride(true)
599             .setScope(ScopeNode.PUBLIC)
600             .setReturnType(stringMapType)
601             .setName("getFieldValuesMap")
602             .setBody(
603                 bodyExprs.stream().map(e -> ExprStatement.withExpr(e)).collect(Collectors.toList()))
604             .setReturnExpr(fieldValuesMapVarExpr)
605             .build();
606 
607     // Method code:
608     // @Override
609     // public String getFieldValue(String fieldName) {
610     //   return getFieldValuesMap().get(fieldName);
611     // }
612     VariableExpr fieldNameVarExpr =
613         VariableExpr.withVariable(
614             Variable.builder().setType(TypeNode.STRING).setName("fieldName").build());
615     MethodDefinition getFieldValueMethod =
616         MethodDefinition.builder()
617             .setIsOverride(true)
618             .setScope(ScopeNode.PUBLIC)
619             .setReturnType(TypeNode.STRING)
620             .setName("getFieldValue")
621             .setArguments(fieldNameVarExpr.toBuilder().setIsDecl(true).build())
622             .setReturnExpr(
623                 MethodInvocationExpr.builder()
624                     .setExprReferenceExpr(
625                         MethodInvocationExpr.builder().setMethodName("getFieldValuesMap").build())
626                     .setMethodName("get")
627                     .setArguments(fieldNameVarExpr)
628                     .setReturnType(TypeNode.STRING)
629                     .build())
630             .build();
631 
632     MethodDefinition toStringMethod =
633         MethodDefinition.builder()
634             .setIsOverride(true)
635             .setScope(ScopeNode.PUBLIC)
636             .setReturnType(TypeNode.STRING)
637             .setName("toString")
638             .setReturnExpr(ValueExpr.withValue(StringObjectValue.withValue(toStringValue)))
639             .build();
640 
641     return AnonymousClassExpr.builder()
642         .setType(
643             TypeNode.withReference(
644                 ConcreteReference.withClazz(com.google.api.resourcenames.ResourceName.class)))
645         .setMethods(Arrays.asList(getFieldValuesMapMethod, getFieldValueMethod, toStringMethod))
646         .build();
647   }
648 
constructValueMatchingPattern(String fieldName, String pattern)649   public static String constructValueMatchingPattern(String fieldName, String pattern) {
650     if (pattern == null || pattern.isEmpty()) {
651       return fieldName + fieldName.hashCode();
652     }
653 
654     final String suffix = "-" + (Math.abs((fieldName + pattern).hashCode()) % 10000);
655 
656     String value = pattern.replace("**", "*");
657 
658     String prevTempl = null;
659     while (!value.equals(prevTempl)) {
660       prevTempl = value;
661       value = REPLACER_PATTERN.matcher(value).replaceFirst("$1$2$1" + suffix);
662     }
663 
664     return value.replace("*", fieldName + suffix);
665   }
666 }
667