io.micrometer.core.ipc.http.HttpSender Maven / Gradle / Ivy
/**
* Copyright 2018 VMware, Inc.
*
* 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
*
* https://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.micrometer.core.ipc.http;
import io.micrometer.core.instrument.util.JsonUtils;
import io.micrometer.core.instrument.util.StringUtils;
import io.micrometer.core.lang.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.zip.GZIPOutputStream;
/**
* A general-purpose interface for controlling how {@link io.micrometer.core.instrument.MeterRegistry} implementations
* perform HTTP calls for various purposes. This interface can be used to inject more advanced customization like SSL
* verification, key loading, etc. without requiring further additions to registry configurations.
*
* @author Jon Schneider
* @since 1.1.0
*/
public interface HttpSender {
Response send(Request request) throws Throwable;
default Request.Builder post(String uri) {
return newRequest(uri).withMethod(Method.POST);
}
default Request.Builder head(String uri) {
return newRequest(uri).withMethod(Method.HEAD);
}
default Request.Builder put(String uri) {
return newRequest(uri).withMethod(Method.PUT);
}
default Request.Builder get(String uri) {
return newRequest(uri).withMethod(Method.GET);
}
default Request.Builder delete(String uri) {
return newRequest(uri).withMethod(Method.DELETE);
}
default Request.Builder options(String uri) {
return newRequest(uri).withMethod(Method.OPTIONS);
}
default Request.Builder newRequest(String uri) {
return new Request.Builder(uri, this);
}
class Request {
private final URL url;
private final byte[] entity;
private final Method method;
private final Map requestHeaders;
public Request(URL url, byte[] entity, Method method, Map requestHeaders) {
this.url = url;
this.entity = entity;
this.method = method;
this.requestHeaders = requestHeaders;
}
public URL getUrl() {
return url;
}
public byte[] getEntity() {
return entity;
}
public Method getMethod() {
return method;
}
public Map getRequestHeaders() {
return requestHeaders;
}
public static Builder build(String uri, HttpSender sender) {
return new Builder(uri, sender);
}
@Override
public String toString() {
StringBuilder printed = new StringBuilder(method.toString()).append(" ")
.append(url.toString()).append("\n");
if (entity.length == 0) {
printed.append("");
} else if ("application/json".equals(requestHeaders.get("Content-Type"))) {
printed.append(JsonUtils.prettyPrint(new String(entity)));
} else {
printed.append(new String(entity));
}
return printed.toString();
}
public static class Builder {
private static final String APPLICATION_JSON = "application/json";
private static final String TEXT_PLAIN = "text/plain";
private final URL url;
private final HttpSender sender;
private byte[] entity = new byte[0];
private Method method;
private Map requestHeaders = new LinkedHashMap<>();
Builder(String uri, HttpSender sender) {
try {
this.url = URI.create(uri).toURL();
} catch (MalformedURLException ex) {
throw new UncheckedIOException(ex);
}
this.sender = sender;
}
/**
* Add a header to the request.
*
* @param name The name of the header.
* @param value The value of the header.
* @return This request builder.
*/
public final Builder withHeader(String name, String value) {
requestHeaders.put(name, value);
return this;
}
/**
* If user and password are non-empty, set basic authentication on the request.
*
* @param user A user name, if available.
* @param password A password, if available.
* @return This request builder.
*/
public final Builder withBasicAuthentication(@Nullable String user, @Nullable String password) {
if (user != null && StringUtils.isNotBlank(user)) {
String encoded = Base64.getEncoder().encodeToString((user.trim() + ":" + (password == null ? "" : password.trim()))
.getBytes(StandardCharsets.UTF_8));
withHeader("Authorization", "Basic " + encoded);
}
return this;
}
/**
* Set the request body as JSON content type.
*
* @param content The request body.
* @return This request builder.
*/
public final Builder withJsonContent(String content) {
return withContent(APPLICATION_JSON, content);
}
/**
* Set the request body as plain text content type.
*
* @param content The request body.
* @return This request builder.
*/
public final Builder withPlainText(String content) {
return withContent(TEXT_PLAIN, content);
}
/**
* Set the request body.
*
* @param type The value of the "Content-Type" header to add.
* @param content The request body.
* @return This request builder.
*/
public final Builder withContent(String type, String content) {
return withContent(type, content.getBytes(StandardCharsets.UTF_8));
}
/**
* Set the request body.
*
* @param type The value of the "Content-Type" header to add.
* @param content The request body.
* @return This request builder.
*/
public final Builder withContent(String type, byte[] content) {
withHeader("Content-Type", type);
entity = content;
return this;
}
/**
* Add header to accept {@code application/json} data.
*
* @return This request builder.
*/
public Builder acceptJson() {
return accept(APPLICATION_JSON);
}
/**
* Add accept header.
*
* @param type The value of the "Accept" header to add.
* @return This request builder.
*/
public Builder accept(String type) {
return withHeader("Accept", type);
}
/**
* Set the request method.
*
* @param method An HTTP method.
* @return This request builder.
*/
public final Builder withMethod(Method method) {
this.method = method;
return this;
}
/**
* Add a "Content-Encoding" header of "gzip" and compress the request body.
*
* @return This request builder.
* @throws IOException If compression fails.
*/
public final Builder compress() throws IOException {
withHeader("Content-Encoding", "gzip");
this.entity = gzip(entity);
return this;
}
/**
* Add a "Content-Encoding" header of "gzip" and compress the request body when the supplied
* condition is true.
*
* @param when Condition that governs when to compress the request body.
* @return This request builder.
* @throws IOException If compression fails.
*/
public final Builder compressWhen(Supplier when) throws IOException {
if (when.get())
return compress();
return this;
}
private static byte[] gzip(byte[] data) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try (GZIPOutputStream out = new GZIPOutputStream(bos)) {
out.write(data);
}
return bos.toByteArray();
}
public final Builder print() {
System.out.println(new Request(url, entity, method, requestHeaders));
return this;
}
public Response send() throws Throwable {
return sender.send(new Request(url, entity, method, requestHeaders));
}
}
}
class Response {
public static final String NO_RESPONSE_BODY = "";
private final int code;
private final String body;
public Response(int code, @Nullable String body) {
this.code = code;
this.body = StringUtils.isBlank(body) ? NO_RESPONSE_BODY : body;
}
public int code() {
return code;
}
public String body() {
return body;
}
public Response onSuccess(Consumer onSuccess) {
switch (HttpStatusClass.valueOf(code)) {
case INFORMATIONAL:
case SUCCESS:
onSuccess.accept(this);
}
return this;
}
public Response onError(Consumer onError) {
switch (HttpStatusClass.valueOf(code)) {
case CLIENT_ERROR:
case SERVER_ERROR:
onError.accept(this);
}
return this;
}
public boolean isSuccessful() {
switch (HttpStatusClass.valueOf(code)) {
case INFORMATIONAL:
case SUCCESS:
return true;
default:
return false;
}
}
}
enum Method {
GET, HEAD, POST, PUT, DELETE, OPTIONS
}
}