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