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.ResourceDescriptor;
18 import com.google.api.ResourceProto;
19 import com.google.api.generator.engine.ast.TypeNode;
20 import com.google.api.generator.gapic.model.ResourceName;
21 import com.google.api.generator.gapic.utils.ResourceNameConstants;
22 import com.google.api.pathtemplate.PathTemplate;
23 import com.google.common.annotations.VisibleForTesting;
24 import com.google.common.base.Preconditions;
25 import com.google.common.base.Strings;
26 import com.google.protobuf.DescriptorProtos.FieldOptions;
27 import com.google.protobuf.DescriptorProtos.FileOptions;
28 import com.google.protobuf.DescriptorProtos.MessageOptions;
29 import com.google.protobuf.Descriptors.Descriptor;
30 import com.google.protobuf.Descriptors.FieldDescriptor;
31 import com.google.protobuf.Descriptors.FileDescriptor;
32 import java.util.ArrayList;
33 import java.util.HashMap;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Optional;
37 import java.util.Set;
38 
39 public class ResourceNameParser {
40   /** Returns a map of resource types (strings) to ResourceName POJOs. */
parseResourceNames( FileDescriptor fileDescriptor, String javaPackage)41   public static Map<String, ResourceName> parseResourceNames(
42       FileDescriptor fileDescriptor, String javaPackage) {
43     Map<String, ResourceName> resourceNames =
44         parseResourceNamesFromFile(fileDescriptor, javaPackage);
45     String pakkage = fileDescriptor.getOptions().getJavaPackage();
46     if (Strings.isNullOrEmpty(pakkage)) {
47       pakkage = javaPackage;
48     }
49     resourceNames.putAll(parseResourceNamesFromMessages(fileDescriptor.getMessageTypes(), pakkage));
50     return resourceNames;
51   }
52 
53   // Convenience wrapper for uni tests. DO NOT ADD ANY NEW FUNCTIONALITY HERE.
54   @VisibleForTesting
parseResourceNames(FileDescriptor fileDescriptor)55   public static Map<String, ResourceName> parseResourceNames(FileDescriptor fileDescriptor) {
56     String pakkage = TypeParser.getPackage(fileDescriptor);
57     return parseResourceNames(fileDescriptor, pakkage);
58   }
59 
60   // Convenience wrapper for uni tests. DO NOT ADD ANY NEW FUNCTIONALITY HERE.
61   @VisibleForTesting
parseResourceNamesFromFile(FileDescriptor fileDescriptor)62   static Map<String, ResourceName> parseResourceNamesFromFile(FileDescriptor fileDescriptor) {
63     String pakkage = TypeParser.getPackage(fileDescriptor);
64     return parseResourceNamesFromFile(fileDescriptor, pakkage);
65   }
66 
67   @VisibleForTesting
parseResourceNamesFromFile( FileDescriptor fileDescriptor, String javaPackage)68   static Map<String, ResourceName> parseResourceNamesFromFile(
69       FileDescriptor fileDescriptor, String javaPackage) {
70     Map<String, ResourceName> typeStringToResourceNames = new HashMap<>();
71     FileOptions fileOptions = fileDescriptor.getOptions();
72     if (fileOptions.getExtensionCount(ResourceProto.resourceDefinition) <= 0) {
73       return typeStringToResourceNames;
74     }
75     List<ResourceDescriptor> protoResources =
76         fileOptions.getExtension(ResourceProto.resourceDefinition);
77     for (ResourceDescriptor protoResource : protoResources) {
78       Optional<ResourceName> resourceNameModelOpt = createResourceName(protoResource, javaPackage);
79       if (!resourceNameModelOpt.isPresent()) {
80         continue;
81       }
82       ResourceName resourceNameModel = resourceNameModelOpt.get();
83       // Clobber anything if we're creating a new ResourceName from a proto.
84       typeStringToResourceNames.put(resourceNameModel.resourceTypeString(), resourceNameModel);
85     }
86     return typeStringToResourceNames;
87   }
88 
89   @VisibleForTesting
parseResourceNamesFromMessages( List<Descriptor> messageTypeDescriptors, String pakkage)90   static Map<String, ResourceName> parseResourceNamesFromMessages(
91       List<Descriptor> messageTypeDescriptors, String pakkage) {
92     Map<String, ResourceName> resourceNames = new HashMap<>();
93     for (Descriptor messageTypeDescriptor : messageTypeDescriptors) {
94       Optional<ResourceName> resourceNameModelOpt =
95           parseResourceNameFromMessageType(messageTypeDescriptor, pakkage);
96       if (resourceNameModelOpt.isPresent()) {
97         ResourceName resourceName = resourceNameModelOpt.get();
98         resourceNames.put(resourceName.resourceTypeString(), resourceName);
99       }
100     }
101     return resourceNames;
102   }
103 
104   @VisibleForTesting
parseResourceNameFromMessageType( Descriptor messageTypeDescriptor, String pakkage)105   static Optional<ResourceName> parseResourceNameFromMessageType(
106       Descriptor messageTypeDescriptor, String pakkage) {
107     MessageOptions messageOptions = messageTypeDescriptor.getOptions();
108     if (!messageOptions.hasExtension(ResourceProto.resource)) {
109       return Optional.empty();
110     }
111 
112     ResourceDescriptor protoResource = messageOptions.getExtension(ResourceProto.resource);
113     // Validation - check that a resource name field is present.
114     if (Strings.isNullOrEmpty(protoResource.getNameField())) {
115       // aip.dev/4231
116       boolean resourceNameFieldFound =
117           messageTypeDescriptor.findFieldByName(ResourceNameConstants.NAME_FIELD_NAME) != null;
118       // If this is null, look for a field with a resource reference is found.
119       // Example: AccountBudgetProposal.
120       for (FieldDescriptor fieldDescriptor : messageTypeDescriptor.getFields()) {
121         FieldOptions fieldOptions = fieldDescriptor.getOptions();
122         if (fieldOptions.hasExtension(ResourceProto.resourceReference)) {
123           resourceNameFieldFound = true;
124           break;
125         }
126       }
127       Preconditions.checkState(
128           resourceNameFieldFound,
129           String.format(
130               "Message %s has a resource annotation but no field titled \"name\" or containing a"
131                   + " resource reference",
132               messageTypeDescriptor.getName()));
133     }
134 
135     TypeNode javaMessageType = TypeParser.parseType(messageTypeDescriptor);
136     return createResourceName(protoResource, pakkage, javaMessageType.reference().fullName());
137   }
138 
createResourceName( ResourceDescriptor protoResource, String pakkage)139   private static Optional<ResourceName> createResourceName(
140       ResourceDescriptor protoResource, String pakkage) {
141     return createResourceName(protoResource, pakkage, null);
142   }
143 
createResourceName( ResourceDescriptor protoResource, String pakkage, String parentMessageName)144   private static Optional<ResourceName> createResourceName(
145       ResourceDescriptor protoResource, String pakkage, String parentMessageName) {
146     // We may need to modify this list.
147     List<String> patterns = new ArrayList<>(protoResource.getPatternList());
148     Preconditions.checkState(
149         !patterns.isEmpty(),
150         String.format(
151             "Resource name definition for %s must have at least one pattern",
152             protoResource.getType()));
153 
154     if (patterns.size() == 1 && patterns.get(0).equals(ResourceNameConstants.WILDCARD_PATTERN)) {
155       return Optional.of(ResourceName.createWildcard(protoResource.getType(), pakkage));
156     }
157 
158     // Assuming that both patterns end with the same variable name.
159     Optional<String> resourceVariableNameOpt = Optional.empty();
160     for (int i = 0; i < patterns.size(); i++) {
161       resourceVariableNameOpt = getVariableNameFromPattern(patterns.get(i));
162       if (resourceVariableNameOpt.isPresent()) {
163         break;
164       }
165     }
166     Preconditions.checkState(
167         resourceVariableNameOpt.isPresent(),
168         String.format("Resource variable name not found in patterns %s", patterns));
169 
170     if (patterns.contains(ResourceNameConstants.WILDCARD_PATTERN)) {
171       patterns.remove(ResourceNameConstants.WILDCARD_PATTERN);
172     }
173 
174     return Optional.of(
175         ResourceName.builder()
176             .setVariableName(resourceVariableNameOpt.get())
177             .setPakkage(pakkage)
178             .setResourceTypeString(protoResource.getType())
179             .setPatterns(patterns)
180             .setParentMessageName(parentMessageName)
181             .build());
182   }
183 
184   @VisibleForTesting
getVariableNameFromPattern(String pattern)185   static Optional<String> getVariableNameFromPattern(String pattern) {
186     // Expected to be small (e.g. less than 10) most of the time.
187     String resourceVariableName = null;
188     String[] tokens = pattern.split("/");
189     String lastToken = tokens[tokens.length - 1];
190     if (lastToken.equals(ResourceNameConstants.DELETED_TOPIC_LITERAL)) {
191       resourceVariableName = lastToken;
192     } else if (lastToken.equals(ResourceNameConstants.WILDCARD_PATTERN)) {
193       resourceVariableName = null;
194     } else {
195       // Allow singleton patterns like projects/{project}/cmekSettings.
196       if (!lastToken.contains("{")) {
197         resourceVariableName = lastToken;
198       } else {
199         Set<String> variableNames = PathTemplate.create(pattern).vars();
200         for (String variableName : variableNames) {
201           if (lastToken.contains(variableName)) {
202             resourceVariableName = variableName;
203             break;
204           }
205         }
206       }
207     }
208     return Strings.isNullOrEmpty(resourceVariableName)
209         ? Optional.empty()
210         : Optional.of(resourceVariableName);
211   }
212 }
213