org.davidmoten.oa3.codegen.http.Http Maven / Gradle / Ivy
package org.davidmoten.oa3.codegen.http;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.davidmoten.oa3.codegen.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.exc.StreamReadException;
import com.fasterxml.jackson.databind.DatabindException;
import com.github.davidmoten.guavamini.Maps;
import com.github.davidmoten.guavamini.Preconditions;
import com.github.davidmoten.guavamini.annotations.VisibleForTesting;
public final class Http {
private static Logger log = LoggerFactory.getLogger(Http.class);
public static Builder method(HttpMethod method) {
return new Builder(method);
}
public static final class Builder {
private final HttpMethod method;
private String basePath;
private String path;
private final Headers headers = Headers.create();
private final List values = new ArrayList<>();
private final List responseDescriptors = new ArrayList<>();
private Serializer serializer;
private List interceptors = new ArrayList<>();
private boolean allowPatch = false;
Builder(HttpMethod method) {
this.method = method;
}
public BuilderWithBasePath basePath(String basePath) {
this.basePath = basePath;
return new BuilderWithBasePath(this);
}
public Builder header(String key, Object value) {
if ("CONTENT-TYPE".equals(key.toUpperCase(Locale.ENGLISH))) {
throw new IllegalArgumentException("set content type in the builder just after setting the body");
}
if (value != null) {
headers.put(key, value.toString());
}
return this;
}
public Builder header(String key, Optional> value) {
if (value.isPresent()) {
return header(key, value.get());
} else {
return this;
}
}
public Builder allowPatch() {
return allowPatch(true);
}
private Builder allowPatch(boolean allowPatch) {
this.allowPatch = allowPatch;
return this;
}
public Builder interceptor(Interceptor interceptor) {
this.interceptors.add(interceptor);
return this;
}
public Builder interceptors(Iterable extends Interceptor> list) {
interceptors.forEach(x -> interceptor(x));
return this;
}
public Builder acceptApplicationJson() {
return header("Accept", "application/json");
}
public Builder acceptAny() {
return header("Accept", "*/*");
}
public Builder param(String name, Optional> value, ParameterType type, Optional contentType) {
return param(name, value, type, contentType, Optional.empty());
}
public Builder param(String name, Optional> value, ParameterType type, Optional contentType,
Optional filename) {
values.add(new ParameterValue(name, value, type, contentType, filename));
return this;
}
public Builder queryParam(String name, Optional> value) {
values.add(ParameterValue.query(name, value));
return this;
}
public Builder queryParam(String name, Object value) {
values.add(ParameterValue.query(name, value));
return this;
}
public Builder pathParam(String name, Optional> value) {
values.add(ParameterValue.path(name, value));
return this;
}
public Builder pathParam(String name, Object value) {
values.add(ParameterValue.path(name, value));
return this;
}
public Builder cookie(String name, Object value) {
values.add(ParameterValue.cookie(name, value));
return this;
}
public BuilderWithBody body(Object value) {
return new BuilderWithBody(this, value);
}
public Builder multipartFormData(Object formData) {
return new BuilderWithBody(this, formData).contentTypeMultipartFormData();
}
public BuilderWithReponseDescriptor responseAs(Class> cls) {
return new BuilderWithReponseDescriptor(this, cls);
}
public HttpResponse call() {
return Http.call(method, basePath, path, serializer, interceptors, headers, values, responseDescriptors,
allowPatch);
}
}
public static final class BuilderWithBasePath {
private final Builder b;
BuilderWithBasePath(Builder b) {
this.b = b;
}
public BuilderWithPath path(String path) {
b.path = path;
return new BuilderWithPath(b);
}
}
public static final class BuilderWithPath {
private final Builder b;
BuilderWithPath(Builder b) {
this.b = b;
}
public Builder serializer(Serializer serializer) {
b.serializer = serializer;
return b;
}
}
public static final class BuilderWithBody {
private final Builder b;
private final Object body;
BuilderWithBody(Builder b, Object body) {
this.b = b;
this.body = body;
}
public Builder contentType(String value) {
b.values.add(ParameterValue.body(body, value));
return b;
}
public Builder contentTypeApplicationJson() {
return contentType("application/json");
}
public Builder contentTypeMultipartFormData() {
return contentType("multipart/form-data");
}
}
public static final class BuilderWithReponseDescriptor {
private final Builder b;
private String statusCode;
private Class> cls;
BuilderWithReponseDescriptor(Builder b, Class> cls) {
this.b = b;
this.cls = cls;
}
public BuilderWithStatusCodeMatch whenStatusCodeMatches(String statusCode) {
this.statusCode = statusCode;
return new BuilderWithStatusCodeMatch(this);
}
public BuilderWithStatusCodeMatch whenStatusCodeDefault() {
this.statusCode = "default";
return new BuilderWithStatusCodeMatch(this);
}
}
public static final class BuilderWithStatusCodeMatch {
private final BuilderWithReponseDescriptor brd;
public BuilderWithStatusCodeMatch(BuilderWithReponseDescriptor brd) {
this.brd = brd;
}
public Builder whenContentTypeMatches(String contentType) {
brd.b.responseDescriptors.add(new ResponseDescriptor(brd.statusCode, contentType, brd.cls));
return brd.b;
}
}
public static HttpResponse call(//
HttpMethod method, //
String basePath, //
String pathTemplate, //
Serializer serializer, //
List interceptors, //
Headers requestHeaders, //
List parameters, //
// (statusCode, contentType, class)
List descriptors, boolean allowPatch) {
return call(method, basePath, pathTemplate, serializer, interceptors, requestHeaders, parameters,
(statusCode, contentType) -> match(descriptors, statusCode, contentType), allowPatch);
}
private static Optional> match(List descriptors, Integer statusCode,
String contentType) {
List matches = new ArrayList<>();
for (ResponseDescriptor d : descriptors) {
if (d.matches(statusCode, contentType)) {
matches.add(d);
}
}
Collections.sort(matches, ResponseDescriptor.specificity());
return matches.stream().findFirst().map(d -> d.cls());
}
private static HttpResponse call(//
HttpMethod method, //
String basePath, //
String pathTemplate, //
Serializer serializer, //
List interceptors, //
Headers requestHeaders, //
List parameters, //
// (statusCode x contentType) -> class
BiFunction super Integer, ? super String, Optional>> responseCls, boolean allowPatch) {
String url = buildUrl(basePath, pathTemplate, parameters);
Optional requestBody = parameters.stream().filter(x -> x.type() == ParameterType.BODY)
.findFirst();
try {
Headers headers = new Headers(requestHeaders);
final HttpMethod requestMethod;
if (!allowPatch && method.equals(HttpMethod.PATCH)) {
headers.put("X-HTTP-Method-Override", HttpMethod.PATCH.name());
requestMethod = HttpMethod.POST;
} else {
requestMethod = method;
}
// modify request metadata (like insert auth related headers)
RequestBase r = new RequestBase(requestMethod, url, headers);
for (Interceptor interceptor : interceptors) {
r = interceptor.intercept(r);
}
log.debug("connecting to method=" + r.method() + ", url=" + url + ", headers=" + r.headers());
return connectAndProcess(serializer, parameters, responseCls, r.url(), requestBody, r.headers(),
r.method());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static HttpResponse connectAndProcess(Serializer serializer, List parameters,
BiFunction super Integer, ? super String, Optional>> responseCls, String url,
Optional requestBody, Headers headers, final HttpMethod method)
throws IOException, MalformedURLException, ProtocolException, StreamReadException, DatabindException {
log.debug("Http.headers={}", headers);
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
con.setRequestMethod(method.name());
parameters.stream() //
.filter(p -> p.type() == ParameterType.HEADER && p.value().isPresent()) //
.forEach(p -> headers.put(p.name(), String.valueOf(p.value().get())));
// add request body content type (should just be one)
parameters.stream().filter(p -> p.contentType().isPresent())
.forEach(p -> headers.put("Content-Type", p.contentType().get()));
headers.forEach((key, list) -> {
con.setRequestProperty(key, list.stream().collect(Collectors.joining(", ")));
});
con.setDoInput(true);
if (requestBody.isPresent()) {
Optional> body = requestBody.get().value();
if (body.isPresent()) {
boolean isMultipartFormData = MediaType.isMultipartFormData(requestBody.get().contentType().orElse(""));
if (isMultipartFormData) {
String boundary = Multipart.randomBoundary();
con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
Map map = properties(body.get());
Multipart.Builder b = Multipart.builder();
map.forEach((name, value) -> {
if (value != null) {
final String contentType;
final Object v;
if (value instanceof HasEncoding) {
contentType = ((HasStringValue) ((HasEncoding) value).contentType()).value();
v = ((HasEncoding) value).value();
} else {
contentType = "application/json";
v = value;
}
b.addFormEntry(name, serializer.serialize(v, contentType), Optional.empty(),
Optional.of(contentType));
}
});
byte[] multipartContent = b.multipartContent(boundary);
// we add 2 to length because HttpURLConnection will add \r\n after headers
con.setRequestProperty("Content-Length", String.valueOf(multipartContent.length + 2));
con.setDoOutput(true);
try (OutputStream out = con.getOutputStream()) {
serializer.serialize(multipartContent, "application/octet-stream", out);
}
} else {
con.setDoOutput(true);
try (OutputStream out = con.getOutputStream()) {
serializer.serialize(body.get(), requestBody.get().contentType().get(), out);
}
}
}
}
int statusCode = con.getResponseCode();
Headers responseHeaders = Headers.create(con.getHeaderFields());
String responseContentType = Optional.ofNullable(con.getHeaderField("Content-Type"))
.orElse("application/octet-stream");
Object data;
Optional> responseClass = responseCls.apply(statusCode, responseContentType);
try (InputStream in = log(con.getInputStream())) {
data = readResponse(serializer, responseClass, responseContentType, in);
} catch (IOException e) {
try (InputStream err = log(con.getErrorStream())) {
data = readResponse(serializer, responseClass, responseContentType, err);
}
}
return new HttpResponse(statusCode, responseHeaders, Optional.of(data));
}
@SuppressWarnings("unchecked")
private static Map properties(Object o) {
try {
Method method = o.getClass().getDeclaredMethod("_internal_properties");
method.setAccessible(true);
return (Map) method.invoke(o);
} catch (IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
return Maps.empty();
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static InputStream log(InputStream inputStream) {
if (!log.isDebugEnabled()) {
return inputStream;
}
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
return new InputStream() {
@Override
public int read() throws IOException {
int v = inputStream.read();
if (v == -1) {
log.debug("Http.inputStream=\n{}", new String(bytes.toByteArray(), StandardCharsets.UTF_8));
}
bytes.write(v);
return v;
}
};
}
@VisibleForTesting
static String buildUrl(String basePath, String pathTemplate, List parameters) {
Preconditions.checkArgument(pathTemplate.startsWith("/"));
// substitute path parameters
String path = stripFinalSlash(basePath) + insertParametersIntoPath(pathTemplate, parameters);
// build query string
String queryString = parameters //
.stream() //
.filter(p -> p.type() == ParameterType.QUERY) //
.filter(p -> p.value().isPresent()) //
.map(p -> urlEncode(p.name()) + "=" + p.value().map(x -> valueToString(x)).orElse("")) //
.collect(Collectors.joining("&"));
return path + "?" + queryString;
}
private static Object readResponse(Serializer serializer, Optional> responseType,
String responseContentType, InputStream in) throws IOException, StreamReadException, DatabindException {
if (responseType.isPresent()) {
return serializer.deserialize(responseType.get(), responseContentType, in);
} else {
return new String(Util.read(in), StandardCharsets.UTF_8);
}
}
private static String valueToString(Object value) {
if (value == null) {
return "";
} else if (value instanceof Collection) {
Collection> c = (Collection>) value;
return c.stream().map(x -> valueToString(x)).collect(Collectors.joining(","));
} else {
return urlEncode(value.toString());
}
}
private static String stripFinalSlash(String s) {
if (s.endsWith("/")) {
return s.substring(0, s.length() - 2);
} else {
return s;
}
}
private static String urlEncode(String s) {
try {
return URLEncoder.encode(s, "UTF-8").replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static String insertParametersIntoPath(String pathTemplate, List parameters) {
String s = pathTemplate;
for (ParameterValue p : parameters) {
if (p.type() == ParameterType.PATH) {
s = insertParameter(s, p.name(), p.value().get());
}
}
return s;
}
private static String insertParameter(String s, String name, Object object) {
return s.replace("{" + name + "}", urlEncode(object.toString()));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy