io.github.selcukes.commons.http.WebClient Maven / Gradle / Ivy
/*
* Copyright (c) Ramesh Babu Prudhvi.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 io.github.selcukes.commons.http;
import io.github.selcukes.collections.Maps;
import io.github.selcukes.collections.Resources;
import io.github.selcukes.databind.utils.JsonUtils;
import lombok.Singular;
import lombok.SneakyThrows;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import static java.net.http.HttpRequest.BodyPublisher;
import static java.net.http.HttpRequest.BodyPublishers;
import static java.net.http.HttpResponse.BodyHandlers.ofString;
public class WebClient {
private HttpClient.Builder clientBuilder;
private HttpRequest.Builder requestBuilder;
private BodyPublisher bodyPublisher;
@Singular
private final Map cookies;
@Singular
private final Map queryParams;
private final String baseUri;
private String endpoint;
public WebClient(String baseUri) {
clientBuilder = HttpClient.newBuilder();
requestBuilder = HttpRequest.newBuilder();
queryParams = new ConcurrentHashMap<>();
cookies = new ConcurrentHashMap<>();
this.baseUri = Objects.requireNonNull(baseUri, "baseUri must not be null");
this.endpoint = "";
}
/**
* Sets the endpoint for the request.
*
* @param endpoint The endpoint to set.
* @return The WebClient object.
*/
public WebClient endpoint(String endpoint) {
this.endpoint = endpoint;
return this;
}
/**
* Adds a cookie to the request.
*
* @param name The name of the cookie.
* @param value The value of the cookie.
* @return The WebClient object.
*/
public WebClient cookie(String name, String value) {
cookies.put(name, value);
return this;
}
/**
* Sets the body of the request.
*
* @param payload The payload to be set as the request body.
* @return The WebClient object.
*/
public WebClient body(final Object payload) {
this.bodyPublisher = bodyPublisher(payload);
return this;
}
/**
* This function creates a GET request and executes it.
*
* @return A Response object.
*/
public WebResponse get() {
requestBuilder.GET();
return execute();
}
/**
* "This function takes an object, serializes it to JSON, and sends it to
* the server as the body of a POST request."
*
* The first line of the function sets the content type of the request to
* "application/json". This is a standard content type for JSON data
*
* @param payload The object to be serialized and sent as the request body.
* @return A Response object
*/
@SneakyThrows
public WebResponse post(final Object payload) {
contentType("application/json")
.body(payload);
requestBuilder.POST(bodyPublisher);
return execute();
}
/**
* This function creates a POST request with the given body and executes it.
*
* @return A Response object.
*/
@SneakyThrows
public WebResponse post() {
requestBuilder.POST(bodyPublisher);
return execute();
}
/**
* This function builds a DELETE request and executes it.
*
* @return A Response object.
*/
public WebResponse delete() {
requestBuilder.DELETE();
return execute();
}
/**
* "Create a PUT request with the given payload, and execute it."
*
* The first line creates a new `HttpRequest` object. The `requestBuilder`
* object is a member variable of the `HttpClient` class. It's a
* `HttpRequest.Builder` object, and it's used to create new `HttpRequest`
* objects
*
* @param payload The payload to be sent to the server.
* @return A Response object
*/
public WebResponse put(final Object payload) {
body(payload);
requestBuilder.PUT(bodyPublisher);
return execute();
}
@SneakyThrows
private BodyPublisher bodyPublisher(final Object payload) {
if (payload instanceof String payloadString) {
bodyPublisher = BodyPublishers.ofString(payloadString);
} else if (payload instanceof Path filePath) {
bodyPublisher = BodyPublishers.ofFile(filePath);
} else {
bodyPublisher = BodyPublishers.ofString(JsonUtils.toJson(payload));
}
return bodyPublisher;
}
@SneakyThrows
private BodyPublisher multiPartBody(final Map data, final String boundary) {
var byteArrays = new ArrayList();
byte[] separator = ("--" + boundary
+ "\r\nContent-Disposition: form-data; name=")
.getBytes(StandardCharsets.UTF_8);
for (var entry : data.entrySet()) {
byteArrays.add(separator);
if (entry.getValue() instanceof Path path) {
String mimeType = Files.probeContentType(path);
byteArrays.add(("\"" + entry.getKey() + "\"; filename=\""
+ path.getFileName() + "\"\r\nContent-Type: " + mimeType
+ "\r\n\r\n").getBytes(StandardCharsets.UTF_8));
byteArrays.add(Files.readAllBytes(path));
byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8));
} else {
byteArrays.add(
("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue()
+ "\r\n").getBytes(StandardCharsets.UTF_8));
}
}
byteArrays.add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8));
return BodyPublishers.ofByteArrays(byteArrays);
}
@SneakyThrows
private WebResponse execute() {
if (!cookies.isEmpty()) {
String cookieHeader = Maps.join(cookies, "=", "; ");
requestBuilder = requestBuilder.header("Cookie", cookieHeader);
clientBuilder.followRedirects(HttpClient.Redirect.NORMAL);
}
var request = requestBuilder.uri(buildUri()).build();
var httpResponse = clientBuilder.build().send(request, ofString());
var response = new WebResponse(httpResponse);
response.logIfError();
return response;
}
/**
* If the proxy parameter is a valid URL, then set the proxy host and port
* to the host and port of the URL
*
* @param proxy The proxy to use.
* @return A WebClient object
*/
public WebClient proxy(final String proxy) {
var url = Resources.tryURL(proxy);
url.ifPresent(u -> clientBuilder = clientBuilder
.proxy(ProxySelector.of(new InetSocketAddress(u.getHost(),
u.getPort() == -1 ? 80 : u.getPort()))));
return this;
}
/**
* This function takes a username and password, encodes them in base64, and
* adds them to the header of the request.
*
* @param username The username to use for authentication
* @param password The password to use for authentication
* @return The WebClient object itself.
*/
public WebClient authenticator(final String username, final String password) {
String encodedAuth = Base64.getEncoder()
.encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
header("Authorization", "Basic " + encodedAuth);
return this;
}
/**
* This function adds an Authorization header to the request with the value
* of Bearer Token.
*
* @param token The token you received from the authentication service.
* @return The WebClient object.
*/
public WebClient authenticator(final String token) {
header("Authorization", "Bearer " + token);
return this;
}
/**
* This function adds a header to the request.
*
* @param name The name of the header.
* @param value The value of the header.
* @return The WebClient object
*/
public WebClient header(final String name, final String value) {
requestBuilder = requestBuilder.header(name, value);
return this;
}
/**
* "Set the body of the request to be a multipart form with the given data
* and boundary."
*
* The first thing we do is generate a random boundary. This is a string
* that will be used to separate the different parts of the multipart form
*
* @param data The data to be sent.
* @return A WebClient object
*/
public WebClient multiPart(final Map data) {
String boundary = "-------------" + UUID.randomUUID();
contentType("multipart/form-data; boundary=" + boundary);
bodyPublisher = multiPartBody(data, boundary);
return this;
}
/**
* This function sets the content type of the request to the given type.
*
* @param type The content type to set.
* @return The WebClient object
*/
public WebClient contentType(final String type) {
header("Content-Type", type);
return this;
}
/**
* Adds a query parameter to the request.
*
* This method allows you to include query parameters in your HTTP request.
* The provided name and value are encoded and added to the query parameters
* map.
*
*
* @param name The name of the query parameter.
* @param value The value of the query parameter.
* @return The {@code WebClient} object with the specified query
* parameter added.
*/
public WebClient queryParams(String name, String value) {
queryParams.put(encode(name), encode(value));
return this;
}
private String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
private URI buildUri() {
var joiner = new StringJoiner("&");
queryParams.forEach((key, value) -> joiner.add(encode(key) + "=" + encode(value)));
return URI.create(baseUri + endpoint + (baseUri.contains("?") ? "&" : "?") + joiner);
}
}