1 /* 2 * Copyright 2020 Google LLC 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google LLC nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 package com.google.api.gax.httpjson; 31 32 import com.google.api.core.BetaApi; 33 import com.google.api.core.InternalApi; 34 import com.google.api.pathtemplate.PathTemplate; 35 import com.google.protobuf.Message; 36 import java.util.Arrays; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.stream.Collectors; 40 41 /** Creates parts of a HTTP request from a protobuf message. */ 42 public class ProtoMessageRequestFormatter<RequestT extends Message> 43 implements HttpRequestFormatter<RequestT> { 44 45 private final FieldsExtractor<RequestT, String> requestBodyExtractor; 46 // Using of triple nested generics (which is not pretty) is predetermined by the 47 // Map<String, List<String>> returned value type of the getQueryParamNames interface method 48 // implemented by this class. 49 private final FieldsExtractor<RequestT, Map<String, List<String>>> queryParamsExtractor; 50 private final String rawPath; 51 private final PathTemplate pathTemplate; 52 private final FieldsExtractor<RequestT, Map<String, String>> pathVarsExtractor; 53 private final List<String> additionalRawPaths; 54 private final List<PathTemplate> additionalPathTemplates; 55 ProtoMessageRequestFormatter( FieldsExtractor<RequestT, String> requestBodyExtractor, FieldsExtractor<RequestT, Map<String, List<String>>> queryParamsExtractor, String rawPath, PathTemplate pathTemplate, FieldsExtractor<RequestT, Map<String, String>> pathVarsExtractor, List<String> additionalRawPaths, List<PathTemplate> additionalPathTemplates)56 private ProtoMessageRequestFormatter( 57 FieldsExtractor<RequestT, String> requestBodyExtractor, 58 FieldsExtractor<RequestT, Map<String, List<String>>> queryParamsExtractor, 59 String rawPath, 60 PathTemplate pathTemplate, 61 FieldsExtractor<RequestT, Map<String, String>> pathVarsExtractor, 62 List<String> additionalRawPaths, 63 List<PathTemplate> additionalPathTemplates) { 64 this.requestBodyExtractor = requestBodyExtractor; 65 this.queryParamsExtractor = queryParamsExtractor; 66 this.rawPath = rawPath; 67 this.pathTemplate = pathTemplate; 68 this.pathVarsExtractor = pathVarsExtractor; 69 this.additionalRawPaths = additionalRawPaths; 70 this.additionalPathTemplates = additionalPathTemplates; 71 } 72 73 public static <RequestT extends Message> newBuilder()74 ProtoMessageRequestFormatter.Builder<RequestT> newBuilder() { 75 return new Builder<RequestT>().setAdditionalPaths(); 76 } 77 toBuilder()78 public Builder<RequestT> toBuilder() { 79 return new Builder<RequestT>() 80 .setPath(rawPath, pathVarsExtractor) 81 .setAdditionalPaths(additionalRawPaths.toArray(new String[] {})) 82 .setQueryParamsExtractor(queryParamsExtractor) 83 .setRequestBodyExtractor(requestBodyExtractor); 84 } 85 86 /* {@inheritDoc} */ 87 @Override getQueryParamNames(RequestT apiMessage)88 public Map<String, List<String>> getQueryParamNames(RequestT apiMessage) { 89 return queryParamsExtractor.extract(apiMessage); 90 } 91 92 /* {@inheritDoc} */ 93 @Override getRequestBody(RequestT apiMessage)94 public String getRequestBody(RequestT apiMessage) { 95 return requestBodyExtractor.extract(apiMessage); 96 } 97 98 /** 99 * Returns the relative URL path created from the path parameters from the given message. Attempts 100 * to match the with the default PathTemplate. If there is not match, it attempts to match with 101 * the templates in the additionalPathTemplates. 102 * 103 * @param apiMessage Request object to extract fields from 104 * @return Path of a matching valid URL or the default Path URL 105 */ 106 @Override getPath(RequestT apiMessage)107 public String getPath(RequestT apiMessage) { 108 Map<String, String> pathVarsMap = pathVarsExtractor.extract(apiMessage); 109 String path = pathTemplate.instantiate(pathVarsMap); 110 if (pathTemplate.matches(path)) { 111 return path; 112 } 113 for (PathTemplate additionalPathTemplate : additionalPathTemplates) { 114 String additionalPath = additionalPathTemplate.instantiate(pathVarsMap); 115 if (additionalPathTemplate.matches(additionalPath)) { 116 return additionalPath; 117 } 118 } 119 // If there are no matches, we return the default path, this is for backwards compatibility. 120 // TODO: Log this scenario once we implemented the Cloud SDK logging. 121 return path; 122 } 123 124 @BetaApi 125 @Override getAdditionalPathTemplates()126 public List<PathTemplate> getAdditionalPathTemplates() { 127 return additionalPathTemplates; 128 } 129 130 /* {@inheritDoc} */ 131 @Override getPathTemplate()132 public PathTemplate getPathTemplate() { 133 return pathTemplate; 134 } 135 136 // This has class has compound setter methods (multiple arguments in setters), that is why not 137 // using @AutoValue. 138 public static class Builder<RequestT extends Message> { 139 private FieldsExtractor<RequestT, String> requestBodyExtractor; 140 private FieldsExtractor<RequestT, Map<String, List<String>>> queryParamsExtractor; 141 private String rawPath; 142 private FieldsExtractor<RequestT, Map<String, String>> pathVarsExtractor; 143 private List<String> rawAdditionalPaths; 144 setRequestBodyExtractor( FieldsExtractor<RequestT, String> requestBodyExtractor)145 public Builder<RequestT> setRequestBodyExtractor( 146 FieldsExtractor<RequestT, String> requestBodyExtractor) { 147 this.requestBodyExtractor = requestBodyExtractor; 148 return this; 149 } 150 setQueryParamsExtractor( FieldsExtractor<RequestT, Map<String, List<String>>> queryParamsExtractor)151 public Builder<RequestT> setQueryParamsExtractor( 152 FieldsExtractor<RequestT, Map<String, List<String>>> queryParamsExtractor) { 153 this.queryParamsExtractor = queryParamsExtractor; 154 return this; 155 } 156 setPath( String rawPath, FieldsExtractor<RequestT, Map<String, String>> pathVarsExtractor)157 public Builder<RequestT> setPath( 158 String rawPath, FieldsExtractor<RequestT, Map<String, String>> pathVarsExtractor) { 159 this.rawPath = rawPath; 160 this.pathVarsExtractor = pathVarsExtractor; 161 return this; 162 } 163 164 @BetaApi setAdditionalPaths(String... rawAdditionalPaths)165 public Builder<RequestT> setAdditionalPaths(String... rawAdditionalPaths) { 166 this.rawAdditionalPaths = Arrays.asList(rawAdditionalPaths); 167 return this; 168 } 169 170 @InternalApi updateRawPath(String rawPath)171 public Builder<RequestT> updateRawPath(String rawPath) { 172 this.rawPath = rawPath; 173 return this; 174 } 175 176 @InternalApi updateRawPath(String target, String replacement)177 public Builder<RequestT> updateRawPath(String target, String replacement) { 178 this.rawPath = this.rawPath.replace(target, replacement); 179 return this; 180 } 181 build()182 public ProtoMessageRequestFormatter<RequestT> build() { 183 return new ProtoMessageRequestFormatter<>( 184 requestBodyExtractor, 185 queryParamsExtractor, 186 rawPath, 187 PathTemplate.create(rawPath), 188 pathVarsExtractor, 189 rawAdditionalPaths, 190 rawAdditionalPaths.stream().map(PathTemplate::create).collect(Collectors.toList())); 191 } 192 } 193 } 194