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.generator.gapic.model.ResourceName; 18 import com.google.api.generator.gapic.model.ResourceReference; 19 import com.google.api.generator.gapic.utils.JavaStyle; 20 import com.google.api.generator.gapic.utils.ResourceReferenceUtils; 21 import com.google.api.pathtemplate.PathTemplate; 22 import com.google.common.annotations.VisibleForTesting; 23 import com.google.common.base.Preconditions; 24 import com.google.common.base.Strings; 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.HashSet; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Optional; 31 import java.util.Set; 32 import javax.annotation.Nullable; 33 34 public class ResourceReferenceParser { 35 private static final String EMPTY_STRING = ""; 36 private static final String LEFT_BRACE = "{"; 37 private static final String RIGHT_BRACE = "}"; 38 private static final String SLASH = "/"; 39 parseResourceNames( ResourceReference resourceReference, String servicePackage, @Nullable String description, Map<String, ResourceName> resourceNames, Map<String, ResourceName> patternsToResourceNames)40 public static List<ResourceName> parseResourceNames( 41 ResourceReference resourceReference, 42 String servicePackage, 43 @Nullable String description, 44 Map<String, ResourceName> resourceNames, 45 Map<String, ResourceName> patternsToResourceNames) { 46 ResourceName resourceName = null; 47 if (resourceReference.isOnlyWildcard()) { 48 resourceName = ResourceName.createWildcard("*", "com.google.api.wildcard.placeholder"); 49 resourceNames.put(resourceName.resourceTypeString(), resourceName); 50 } else { 51 resourceName = resourceNames.get(resourceReference.resourceTypeString()); 52 } 53 54 // Support older resource_references that specify only the final typename, e.g. FooBar versus 55 // example.com/FooBar. 56 if (resourceReference.resourceTypeString().indexOf(SLASH) < 0) { 57 Optional<String> actualResourceTypeNameOpt = 58 resourceNames.keySet().stream() 59 .filter( 60 k -> 61 k.substring(k.lastIndexOf(SLASH) + 1) 62 .equals(resourceReference.resourceTypeString())) 63 .findFirst(); 64 if (actualResourceTypeNameOpt.isPresent()) { 65 resourceName = resourceNames.get(actualResourceTypeNameOpt.get()); 66 } 67 } else { 68 resourceName = resourceNames.get(resourceReference.resourceTypeString()); 69 } 70 Preconditions.checkNotNull( 71 resourceName, 72 String.format( 73 "No resource definition found for reference with type %s", 74 resourceReference.resourceTypeString())); 75 if (!resourceReference.isChildType() || resourceName.isOnlyWildcard()) { 76 return Arrays.asList(resourceName); 77 } 78 79 // Create a parent ResourceName for each pattern. 80 List<ResourceName> parentResourceNames = new ArrayList<>(); 81 Set<String> resourceTypeStrings = new HashSet<>(); 82 83 for (String pattern : resourceName.patterns()) { 84 Optional<ResourceName> parentResourceNameOpt = 85 parseParentResourceName( 86 pattern, 87 servicePackage, 88 resourceName.pakkage(), 89 resourceName.resourceTypeString(), 90 description, 91 patternsToResourceNames); 92 // Prevent duplicates. 93 if (parentResourceNameOpt.isPresent() 94 && !resourceTypeStrings.contains(parentResourceNameOpt.get().resourceTypeString())) { 95 ResourceName parentResourceName = parentResourceNameOpt.get(); 96 parentResourceNames.add(parentResourceName); 97 resourceTypeStrings.add(parentResourceName.resourceTypeString()); 98 } 99 } 100 return parentResourceNames; 101 } 102 103 @VisibleForTesting parseParentResourceName( String pattern, String servicePackage, String resourcePackage, String resourceTypeString, @Nullable String description, Map<String, ResourceName> patternsToResourceNames)104 static Optional<ResourceName> parseParentResourceName( 105 String pattern, 106 String servicePackage, 107 String resourcePackage, 108 String resourceTypeString, 109 @Nullable String description, 110 Map<String, ResourceName> patternsToResourceNames) { 111 Optional<String> parentPatternOpt = ResourceReferenceUtils.parseParentPattern(pattern); 112 if (!parentPatternOpt.isPresent()) { 113 return Optional.empty(); 114 } 115 116 String parentPattern = parentPatternOpt.get(); 117 if (patternsToResourceNames.get(parentPattern) != null) { 118 return Optional.of(patternsToResourceNames.get(parentPattern)); 119 } 120 121 String[] tokens = parentPattern.split(SLASH); 122 int numTokens = tokens.length; 123 String lastToken = tokens[numTokens - 1]; 124 Set<String> variableNames = PathTemplate.create(parentPattern).vars(); 125 String parentVariableName = null; 126 // Try the extracting from the conventional pattern first. 127 // E.g. Profile is the parent of users/{user}/profiles/{profile}/blurbs/{blurb}. 128 for (String variableName : variableNames) { 129 if (lastToken.contains(variableName)) { 130 parentVariableName = variableName; 131 } 132 } 133 134 // TODO(miraleung): Add unit tests that exercise these edge cases. 135 // Check unconventional patterns. 136 // Assume that non-slash separators will only ever appear in the last component of a patetrn. 137 // That is, they will not appear in the parent components under consideration. 138 if (Strings.isNullOrEmpty(parentVariableName)) { 139 String lowerTypeName = 140 resourceTypeString.substring(resourceTypeString.indexOf(SLASH) + 1).toLowerCase(); 141 // Check for the parent of users/{user}/profile/blurbs/legacy/{legacy_user}~{blurb}. 142 // We're curerntly at users/{user}/profile/blurbs. 143 if ((lastToken.endsWith("s") || lastToken.contains(lowerTypeName)) && numTokens > 2) { 144 // Not the singleton we're looking for, back up. 145 parentVariableName = tokens[numTokens - 2]; 146 } else { 147 // Check for the parent of users/{user}/profile/blurbs/{blurb}. 148 // We're curerntly at users/{user}/profile. 149 parentVariableName = lastToken; 150 } 151 parentVariableName = 152 parentVariableName.replace(LEFT_BRACE, EMPTY_STRING).replace(RIGHT_BRACE, EMPTY_STRING); 153 } 154 155 Preconditions.checkNotNull( 156 parentVariableName, 157 String.format("Could not parse variable name from pattern %s", parentPattern)); 158 159 // Use the package where the resource was defined, only if that is a sub-package of the 160 // current service (which is assumed to be the project's package). 161 String pakkage = resolvePackages(resourcePackage, servicePackage); 162 String parentResourceTypeString = 163 String.format( 164 "%s/%s", 165 resourceTypeString.substring(0, resourceTypeString.indexOf(SLASH)), 166 JavaStyle.toUpperCamelCase(parentVariableName)); 167 168 ResourceName parentResourceName = 169 ResourceName.builder() 170 .setVariableName(parentVariableName) 171 .setPakkage(pakkage) 172 .setResourceTypeString(parentResourceTypeString) 173 .setPatterns(Arrays.asList(parentPattern)) 174 .setDescription(description) 175 .build(); 176 patternsToResourceNames.put(parentPattern, parentResourceName); 177 178 return Optional.of(parentResourceName); 179 } 180 181 @VisibleForTesting resolvePackages(String resourceNamePackage, String servicePackage)182 static String resolvePackages(String resourceNamePackage, String servicePackage) { 183 return resourceNamePackage.contains(servicePackage) ? resourceNamePackage : servicePackage; 184 } 185 } 186