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