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}