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.AnnotationsProto; 18 import com.google.api.HttpRule; 19 import com.google.api.HttpRule.PatternCase; 20 import com.google.api.generator.gapic.model.Field; 21 import com.google.api.generator.gapic.model.HttpBindings; 22 import com.google.api.generator.gapic.model.HttpBindings.HttpBinding; 23 import com.google.api.generator.gapic.model.Message; 24 import com.google.api.pathtemplate.PathTemplate; 25 import com.google.common.base.Preconditions; 26 import com.google.common.base.Strings; 27 import com.google.common.collect.ImmutableSet; 28 import com.google.common.collect.ImmutableSortedSet; 29 import com.google.common.collect.Sets; 30 import com.google.protobuf.DescriptorProtos.MethodOptions; 31 import com.google.protobuf.Descriptors.MethodDescriptor; 32 import java.util.Collections; 33 import java.util.HashMap; 34 import java.util.Map; 35 import java.util.Optional; 36 import java.util.Set; 37 import java.util.regex.Matcher; 38 import java.util.regex.Pattern; 39 import java.util.stream.Collectors; 40 41 public class HttpRuleParser { 42 private static final String ASTERISK = "*"; 43 private static final Pattern TEMPLATE_VALS_PATTERN = 44 Pattern.compile("\\{(?<var>[\\w.]*)=(?<template>[^}]+)}"); 45 parse( MethodDescriptor protoMethod, Message inputMessage, Map<String, Message> messageTypes)46 public static HttpBindings parse( 47 MethodDescriptor protoMethod, Message inputMessage, Map<String, Message> messageTypes) { 48 MethodOptions methodOptions = protoMethod.getOptions(); 49 if (!methodOptions.hasExtension(AnnotationsProto.http)) { 50 return null; 51 } 52 53 HttpRule httpRule = methodOptions.getExtension(AnnotationsProto.http); 54 55 // Body validation. 56 if (!Strings.isNullOrEmpty(httpRule.getBody()) && !httpRule.getBody().equals(ASTERISK)) { 57 checkHttpFieldIsValid(httpRule.getBody(), inputMessage, true); 58 } 59 60 return parseHttpRuleHelper(httpRule, Optional.of(inputMessage), messageTypes); 61 } 62 parseHttpRule(HttpRule httpRule)63 public static HttpBindings parseHttpRule(HttpRule httpRule) { 64 return parseHttpRuleHelper(httpRule, Optional.empty(), Collections.emptyMap()); 65 } 66 parseHttpRuleHelper( HttpRule httpRule, Optional<Message> inputMessageOpt, Map<String, Message> messageTypes)67 private static HttpBindings parseHttpRuleHelper( 68 HttpRule httpRule, Optional<Message> inputMessageOpt, Map<String, Message> messageTypes) { 69 // Get pattern. 70 String pattern = getHttpVerbPattern(httpRule); 71 ImmutableSet.Builder<String> bindingsBuilder = ImmutableSet.builder(); 72 bindingsBuilder.addAll(PatternParser.getPatternBindings(pattern)); 73 if (httpRule.getAdditionalBindingsCount() > 0) { 74 for (HttpRule additionalRule : httpRule.getAdditionalBindingsList()) { 75 // TODO: save additional bindings path in HttpRuleBindings 76 bindingsBuilder.addAll( 77 PatternParser.getPatternBindings(getHttpVerbPattern(additionalRule))); 78 } 79 } 80 81 Set<String> pathParamNames = bindingsBuilder.build(); 82 Map<String, String> patternSampleValues = constructPathValuePatterns(pattern); 83 84 // TODO: support nested message fields bindings 85 // Nested message fields bindings for query params are already supported as part of 86 // https://github.com/googleapis/gax-java/pull/1784, 87 // however we need to excludes fields that are already configured for path params and body, see 88 // https://github.com/googleapis/googleapis/blob/532289228eaebe77c42438f74b8a5afa85fee1b6/google/api/http.proto#L208 for details, 89 // the current logic does not exclude fields that are more than one level deep. 90 String body = httpRule.getBody(); 91 Set<String> bodyParamNames; 92 Set<String> queryParamNames; 93 if (!inputMessageOpt.isPresent()) { 94 // Must be a mixin, do not support full HttpRuleBindings for now 95 bodyParamNames = ImmutableSet.of(); 96 queryParamNames = ImmutableSet.of(); 97 } else if (Strings.isNullOrEmpty(body)) { 98 bodyParamNames = ImmutableSet.of(); 99 queryParamNames = Sets.difference(inputMessageOpt.get().fieldMap().keySet(), pathParamNames); 100 } else if (body.equals(ASTERISK)) { 101 bodyParamNames = Sets.difference(inputMessageOpt.get().fieldMap().keySet(), pathParamNames); 102 queryParamNames = ImmutableSet.of(); 103 } else { 104 bodyParamNames = ImmutableSet.of(body); 105 Set<String> bodyBinidngsUnion = Sets.union(bodyParamNames, pathParamNames); 106 queryParamNames = 107 Sets.difference(inputMessageOpt.get().fieldMap().keySet(), bodyBinidngsUnion); 108 } 109 110 Message message = inputMessageOpt.orElse(null); 111 return HttpBindings.builder() 112 .setHttpVerb(HttpBindings.HttpVerb.valueOf(httpRule.getPatternCase().toString())) 113 .setPattern(pattern) 114 .setAdditionalPatterns( 115 httpRule.getAdditionalBindingsList().stream() 116 .map(HttpRuleParser::getHttpVerbPattern) 117 .collect(Collectors.toList())) 118 .setPathParameters( 119 validateAndConstructHttpBindings( 120 pathParamNames, message, messageTypes, patternSampleValues)) 121 .setQueryParameters( 122 validateAndConstructHttpBindings(queryParamNames, message, messageTypes, null)) 123 .setBodyParameters( 124 validateAndConstructHttpBindings(bodyParamNames, message, messageTypes, null)) 125 .setIsAsteriskBody(body.equals(ASTERISK)) 126 .build(); 127 } 128 validateAndConstructHttpBindings( Set<String> paramNames, Message inputMessage, Map<String, Message> messageTypes, Map<String, String> patternSampleValues)129 private static Set<HttpBinding> validateAndConstructHttpBindings( 130 Set<String> paramNames, 131 Message inputMessage, 132 Map<String, Message> messageTypes, 133 Map<String, String> patternSampleValues) { 134 ImmutableSortedSet.Builder<HttpBinding> httpBindings = ImmutableSortedSet.naturalOrder(); 135 136 for (String paramName : paramNames) { 137 // Handle foo.bar cases by descending into the subfields. 138 String patternSampleValue = 139 patternSampleValues != null ? patternSampleValues.get(paramName) : null; 140 String[] subFields = paramName.split("\\."); 141 HttpBinding.Builder httpBindingBuilder = HttpBinding.builder().setName(paramName); 142 if (inputMessage == null) { 143 httpBindings.add(httpBindingBuilder.setValuePattern(patternSampleValue).build()); 144 continue; 145 } 146 Message nestedMessage = inputMessage; 147 for (int i = 0; i < subFields.length; i++) { 148 String subFieldName = subFields[i]; 149 if (i < subFields.length - 1) { 150 Field field = nestedMessage.fieldMap().get(subFieldName); 151 nestedMessage = messageTypes.get(field.type().reference().fullName()); 152 Preconditions.checkNotNull( 153 nestedMessage, 154 String.format( 155 "No containing message found for field %s with type %s", 156 field.name(), field.type().reference().simpleName())); 157 158 } else { 159 if (patternSampleValues != null) { 160 checkHttpFieldIsValid(subFieldName, nestedMessage, false); 161 patternSampleValue = patternSampleValues.get(paramName); 162 } 163 Field field = nestedMessage.fieldMap().get(subFieldName); 164 httpBindings.add( 165 httpBindingBuilder.setValuePattern(patternSampleValue).setField(field).build()); 166 } 167 } 168 } 169 return httpBindings.build(); 170 } 171 getHttpVerbPattern(HttpRule httpRule)172 private static String getHttpVerbPattern(HttpRule httpRule) { 173 PatternCase patternCase = httpRule.getPatternCase(); 174 switch (patternCase) { 175 case GET: 176 return httpRule.getGet(); 177 case PUT: 178 return httpRule.getPut(); 179 case POST: 180 return httpRule.getPost(); 181 case DELETE: 182 return httpRule.getDelete(); 183 case PATCH: 184 return httpRule.getPatch(); 185 case CUSTOM: // Invalid pattern. 186 // Fall through. 187 default: 188 return ""; 189 } 190 } 191 checkHttpFieldIsValid(String binding, Message inputMessage, boolean isBody)192 private static void checkHttpFieldIsValid(String binding, Message inputMessage, boolean isBody) { 193 Preconditions.checkState( 194 !Strings.isNullOrEmpty(binding), 195 String.format("Null or empty binding for " + inputMessage.name())); 196 Preconditions.checkState( 197 inputMessage.fieldMap().containsKey(binding), 198 String.format( 199 "Expected message %s to contain field %s but none found", 200 inputMessage.name(), binding)); 201 Field field = inputMessage.fieldMap().get(binding); 202 boolean fieldCondition = !field.isRepeated(); 203 if (!isBody) { 204 fieldCondition &= field.type().isProtoPrimitiveType() || field.isEnum(); 205 } 206 String messageFormat = 207 "Expected a non-repeated " 208 + (isBody ? "" : "primitive ") 209 + "type for field %s in message %s but got type %s"; 210 Preconditions.checkState( 211 fieldCondition, 212 String.format(messageFormat, field.name(), inputMessage.name(), field.type())); 213 } 214 constructPathValuePatterns(String pattern)215 private static Map<String, String> constructPathValuePatterns(String pattern) { 216 Map<String, String> varPattern = new HashMap<>(); 217 if (pattern == null || pattern.isEmpty()) { 218 return varPattern; 219 } 220 Matcher m = TEMPLATE_VALS_PATTERN.matcher(PathTemplate.create(pattern).toString()); 221 222 while (m.find()) { 223 varPattern.put(m.group("var"), m.group("template")); 224 } 225 226 return varPattern; 227 } 228 } 229