nstream.adapter.http.HttpAdapterUtils Maven / Gradle / Ivy
// Copyright 2015-2024 Nstream, inc.
//
// Licensed under the Redis Source Available License 2.0 (RSALv2) Agreement;
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://redis.com/legal/rsalv2-agreement/
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package nstream.adapter.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.function.Function;
import nstream.adapter.common.AdapterUtils;
import nstream.adapter.common.ext.HttpIngressSettings;
import nstream.adapter.common.ingress.AssemblyException;
import swim.http.MediaType;
import swim.structure.Value;
public final class HttpAdapterUtils {
private HttpAdapterUtils() {
}
public static HttpIngressSettings ingressSettingsFromProp(Value prop) {
final HttpIngressSettings settings = HttpIngressSettings.form().cast(prop);
return settings == null ? HttpIngressSettings.defaultSettings() : settings;
}
/**
* Constructs an {@link HttpRequest} based on the provided parameters.
*
* @param method the HTTP method (e.g. "GET", "POST")
* @param endpointUrl an RFC 2396 compliant request URI
* @param headers the request headers
* @param timeoutMillis the request timeout duration in milliseconds
* @return an {@code HttpRequest}
* @throws URISyntaxException if endpointUrl violates RFC 2396
*/
public static HttpRequest buildHttpRequest(String method, String endpointUrl,
Map headers,
HttpRequest.BodyPublisher body,
long timeoutMillis)
throws URISyntaxException {
HttpRequest.Builder builder = HttpRequest.newBuilder(new URI(endpointUrl));
if (headers != null && !headers.isEmpty()) {
for (Map.Entry entry : headers.entrySet()) {
builder = builder.header(entry.getKey(), entry.getValue());
}
}
builder = builder.timeout(Duration.ofMillis(timeoutMillis));
builder = builder.method(method, body);
return builder.build();
}
/**
*
* @param executor
* @param request
* @return
* @throws IOException
* @throws InterruptedException
*/
public static HttpResponse executeHttpRequest(HttpClient executor, HttpRequest request)
throws IOException, InterruptedException {
return executor.send(request, HttpResponse.BodyHandlers.ofInputStream());
}
/**
* Convenience method to extract an HTTP header from an {@link HttpHeaders}
* object.
*
* @param headers the {@code HttpHeaders} object holding the desired header
* @param header the name of the header of interest
* @param transform a single-argument transformation function from the
* header's {@code String} value to the user's desired type
* @param orElse the default {@code V} to return if the header is absent
* @param the output type of {@code transform}
* @return a user-chosen representation of {@code header}
*/
public static V extractHeaderValue(HttpHeaders headers, String header,
Function transform, V orElse) {
return headers.firstValue(header)
.map(transform)
.orElse(orElse);
}
/**
* Decides the content encoding to use given an {@link HttpIngressSettings}
* configuration and a set of received HTTP response headers.
*
* Conflicting information is resolved first in favor of the {@code
* HttpIngressSettings} configuration, then the first matching header value.
*
* @param settings the {@code HttpIngressSettings} configuration
* @param responseHeaders the response headers
* @return the {@code HttpIngressSettings}-preferred resolution of the
* information in the inputs.
*/
public static String contentEncoding(HttpIngressSettings settings, HttpHeaders responseHeaders) {
final String override = settings.contentEncodingOverride();
if (override != null && !override.isEmpty()) {
return override;
}
return extractHeaderValue(responseHeaders, "content-encoding",
Function.identity(), null);
}
/**
* Decides the content type to use given an {@link HttpIngressSettings}
* configuration and a set of received HTTP response headers.
*
*
Conflicting information is resolved first in favor of the {@code
* HttpIngressSettings} configuration, then the first matching header value.
*
* @param settings the {@code HttpIngressSettings} configuration
* @param responseHeaders the response headers
* @return the {@code HttpIngressSettings}-preferred resolution of the
* information in the inputs.
*/
public static MediaType contentType(HttpIngressSettings settings, HttpHeaders responseHeaders) {
final String override = settings.contentTypeOverride();
if (override != null && !override.isEmpty()) {
return MediaType.parse(override);
}
return extractHeaderValue(responseHeaders, "content-type",
MediaType::parse, MediaType.applicationJson());
}
public static InputStream responseBodyStream(HttpResponse response, String encoding)
throws IOException {
return AdapterUtils.decodeStream(response.body(), encoding);
}
public static String minimalContentType(MediaType contentType) {
final String subtype = contentType.subtype();
return subtype.substring(subtype.startsWith("x-") ? 2 : 0);
}
/**
* Transforms the encoding-free
* @param bodyStream
* @param contentType
* @return
* @throws IOException
*/
public static Value responseBodyStructure(InputStream bodyStream, MediaType contentType)
throws AssemblyException, IOException {
if (!contentType.isApplication() && !contentType.isText()) {
throw new IllegalArgumentException("Unsupported content type: " + contentType);
}
final String subtype = minimalContentType(contentType);
return AdapterUtils.assembleContent(bodyStream, subtype);
}
/**
* A no-frills singleton {@link HttpClient}.
*
* The returned client is initialized with Java's default values and thus
* is unsuitable for use alongside, for example, custom proxy selectors and
* SSL contexts. The client is lazily created upon the first invocation of
* this method, thus occupying no resources if the method is never invoked.
*
* @return A singleton, threadsafe {@code HttpClient} instance.
* @see HttpClient#newHttpClient()
*/
public static HttpClient defaultHttpClient() {
return DefaultHttpClient.instance().client();
}
// Thread-safe, efficient pattern for lazy singleton initialization in Java
private static final class DefaultHttpClient {
private final HttpClient client;
private DefaultHttpClient() {
this.client = HttpClient.newHttpClient();
}
private HttpClient client() {
return this.client;
}
private static class DefaultHttpClientHolder {
private static final DefaultHttpClient INSTANCE = new DefaultHttpClient();
}
private static DefaultHttpClient instance() {
return DefaultHttpClientHolder.INSTANCE;
}
}
}