001/** 002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.runtime.manager.service.http; 017 018import static java.util.Optional.ofNullable; 019import static java.util.stream.Collectors.toMap; 020 021import java.io.BufferedOutputStream; 022import java.io.ByteArrayOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.lang.reflect.Type; 026import java.net.HttpURLConnection; 027import java.net.URL; 028import java.nio.charset.StandardCharsets; 029import java.util.List; 030import java.util.Map; 031import java.util.Objects; 032import java.util.Optional; 033import java.util.TreeMap; 034import java.util.function.BiFunction; 035 036import org.talend.sdk.component.api.service.http.Configurer; 037import org.talend.sdk.component.api.service.http.Decoder; 038import org.talend.sdk.component.api.service.http.HttpException; 039import org.talend.sdk.component.api.service.http.Response; 040import org.talend.sdk.component.runtime.manager.service.http.codec.CodecMatcher; 041 042import lombok.AllArgsConstructor; 043import lombok.Data; 044import lombok.extern.slf4j.Slf4j; 045 046@Slf4j 047@Data 048@AllArgsConstructor 049public class ExecutionContext implements BiFunction<String, Object[], Object> { 050 051 private final HttpRequestCreator requestCreator; 052 053 private final Type responseType; 054 055 private final boolean isResponse; 056 057 private final Map<String, Decoder> decoders; 058 059 public Object apply(final String base, final Object[] params) { 060 HttpURLConnection urlConnection = null; 061 try { 062 final HttpRequest request = requestCreator.apply(base, params); 063 final String queryParams = String.join("&", request.getQueryParams()); 064 final URL url = new URL(request.getUrl() + (queryParams.isEmpty() ? "" : "?" + queryParams)); 065 urlConnection = HttpURLConnection.class.cast(url.openConnection()); 066 urlConnection.setRequestMethod(request.getMethodType()); 067 request.getHeaders().forEach(urlConnection::setRequestProperty); 068 069 final Optional<byte[]> requestBody = request.getBody(); 070 071 final DefaultConnection connection = new DefaultConnection(urlConnection, requestBody.orElse(null), true); 072 if (request.getConfigurer() != null) { 073 request.getConfigurer().configure(connection, request.getConfigurationOptions()); 074 } 075 connection.postConfigure(); 076 077 if (requestBody.isPresent()) { 078 urlConnection.setDoOutput(true); 079 try (final BufferedOutputStream outputStream = 080 new BufferedOutputStream(urlConnection.getOutputStream())) { 081 outputStream.write(requestBody.orElse(null)); 082 outputStream.flush(); 083 } 084 } 085 final int responseCode = urlConnection.getResponseCode(); 086 final CodecMatcher<Decoder> decoderMatcher = new CodecMatcher<>(); 087 final String contentType = urlConnection.getHeaderField("content-type"); 088 final byte[] error; 089 final byte[] response; 090 try { 091 final InputStream inputStream = urlConnection.getInputStream(); 092 if (getResponseType() == InputStream.class) { 093 if (isResponse()) { 094 return new InputStreamResponse(responseCode, PassthroughDecoder.INSTANCE, 095 headers(urlConnection), null, inputStream); 096 } 097 return inputStream; 098 } 099 response = slurp(inputStream, urlConnection.getContentLength()); 100 if (!isResponse()) { 101 return byte[].class == getResponseType() ? response 102 : decoderMatcher.select(getDecoders(), contentType).decode(response, getResponseType()); 103 } 104 return new ResponseImpl(responseCode, 105 byte[].class == getResponseType() ? PassthroughDecoder.INSTANCE 106 : decoderMatcher.select(getDecoders(), contentType), 107 headers(urlConnection), null, response, getResponseType()); 108 } catch (final IOException e) { 109 error = ofNullable(urlConnection.getErrorStream()) 110 .map(s -> slurp(s, -1)) 111 .orElseGet(() -> ofNullable(e.getMessage()) 112 .map(s -> s.getBytes(StandardCharsets.UTF_8)) 113 .orElse(null)); 114 final Response<Object> errorResponse = new ResponseImpl(responseCode, 115 byte[].class == getResponseType() ? PassthroughDecoder.INSTANCE 116 : decoderMatcher.select(getDecoders(), contentType), 117 headers(urlConnection), error, null, getResponseType()); 118 119 if (isResponse()) { 120 return errorResponse; 121 } 122 123 throw new HttpException(errorResponse); 124 } 125 126 } catch (final IOException e) { 127 if (urlConnection != null) { // it fails, release the resources, otherwise we want to be pooled 128 urlConnection.disconnect(); 129 } 130 throw new IllegalStateException(e); 131 } 132 } 133 134 private static byte[] slurp(final InputStream responseStream, final int len) { 135 final byte[] buffer = new byte[8192]; 136 final ByteArrayOutputStream responseBuffer = new ByteArrayOutputStream(len > 0 ? len : buffer.length); 137 try (final InputStream inputStream = responseStream) { 138 int count; 139 while ((count = inputStream.read(buffer)) >= 0) { 140 responseBuffer.write(buffer, 0, count); 141 } 142 } catch (final IOException e) { 143 throw new IllegalStateException(e); 144 } 145 return responseBuffer.toByteArray(); 146 } 147 148 private Map<String, List<String>> headers(final HttpURLConnection urlConnection) { 149 return urlConnection 150 .getHeaderFields() 151 .keySet() 152 .stream() 153 .filter(Objects::nonNull) 154 .collect(toMap(e -> e, urlConnection.getHeaderFields()::get, (k, v) -> { 155 log.warn("Duplicated header key: merging arbitrarily {} vs {}, check peer configuration.", k, v); 156 return k; 157 }, () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))); 158 } 159 160 @AllArgsConstructor 161 private static class DefaultConnection implements Configurer.Connection { 162 163 private final HttpURLConnection urlConnection; 164 165 private final byte[] payload; 166 167 private boolean followRedirects; 168 169 @Override 170 public String getMethod() { 171 return urlConnection.getRequestMethod(); 172 } 173 174 @Override 175 public String getUrl() { 176 return urlConnection.getURL().toExternalForm(); 177 } 178 179 @Override 180 public Map<String, List<String>> getHeaders() { 181 return urlConnection.getHeaderFields(); 182 } 183 184 @Override 185 public byte[] getPayload() { 186 return payload; 187 } 188 189 @Override 190 public Configurer.Connection withHeader(final String name, final String value) { 191 urlConnection.addRequestProperty(name, value); 192 return this; 193 } 194 195 @Override 196 public Configurer.Connection withReadTimeout(final int timeout) { 197 urlConnection.setReadTimeout(timeout); 198 return this; 199 } 200 201 @Override 202 public Configurer.Connection withConnectionTimeout(final int timeout) { 203 urlConnection.setConnectTimeout(timeout); 204 return this; 205 } 206 207 @Override 208 public Configurer.Connection withoutFollowRedirects() { 209 followRedirects = false; 210 return this; 211 } 212 213 private void postConfigure() { 214 urlConnection.setInstanceFollowRedirects(followRedirects); 215 } 216 } 217 218 private static class PassthroughDecoder implements Decoder { 219 220 private static final Decoder INSTANCE = new PassthroughDecoder(); 221 222 @Override 223 public Object decode(final byte[] value, final Type expectedType) { 224 return value; 225 } 226 } 227 228 @AllArgsConstructor 229 private static abstract class BaseResponse<T> implements Response<T> { 230 231 private final int status; 232 233 protected final Decoder decoder; 234 235 private Map<String, List<String>> headers; 236 237 private byte[] error; 238 239 @Override 240 public int status() { 241 return status; 242 } 243 244 @Override 245 public Map<String, List<String>> headers() { 246 return headers; 247 } 248 249 @Override 250 public <E> E error(final Class<E> type) { 251 if (error == null) { 252 return null; 253 } 254 if (String.class == type) { 255 return type.cast(new String(error)); 256 } 257 258 return type.cast(decoder.decode(error, type)); 259 } 260 } 261 262 private static class InputStreamResponse<T> extends BaseResponse<InputStream> { 263 264 private final InputStream inputStream; 265 266 private InputStreamResponse(final int status, final Decoder decoder, final Map<String, List<String>> headers, 267 final byte[] error, final InputStream inputStream) { 268 super(status, decoder, headers, error); 269 this.inputStream = inputStream; 270 } 271 272 @Override 273 public InputStream body() { 274 return inputStream; 275 } 276 } 277 278 private static class ResponseImpl<T> extends BaseResponse<T> { 279 280 private final byte[] responseBody; 281 282 private final Type responseType; 283 284 private volatile T bodyCache; 285 286 private ResponseImpl(final int status, final Decoder decoder, final Map<String, List<String>> headers, 287 final byte[] error, final byte[] responseBody, final Type responseType) { 288 super(status, decoder, headers, error); 289 this.responseBody = responseBody; 290 this.responseType = responseType; 291 } 292 293 @Override 294 public T body() { 295 if (responseBody == null || byte[].class == responseType) { 296 return (T) responseBody; 297 } 298 299 if (bodyCache == null) { 300 synchronized (this) { 301 if (bodyCache == null) { 302 bodyCache = (T) decoder.decode(responseBody, responseType); 303 } 304 } 305 } 306 return bodyCache; 307 } 308 } 309}