All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.davidmoten.oa3.codegen.http.Http Maven / Gradle / Ivy

There is a newer version: 0.1.22
Show newest version
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 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>> 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>> 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