HttpConfigs.java

/**
 * Copyright (C) 2017 HttpBuilder-NG Project
 *
 * 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 groovyx.net.http;

import groovyx.net.http.optional.Csv;
import groovyx.net.http.optional.Html;

import java.io.IOException;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

import static groovyx.net.http.ChainedHttpConfig.Auth;
import static groovyx.net.http.ChainedHttpConfig.AuthType;
import static groovyx.net.http.ChainedHttpConfig.ChainedRequest;
import static groovyx.net.http.ChainedHttpConfig.ChainedResponse;
import static groovyx.net.http.ContentTypes.BINARY;
import static groovyx.net.http.ContentTypes.JSON;
import static groovyx.net.http.ContentTypes.TEXT;
import static groovyx.net.http.ContentTypes.URLENC;
import static groovyx.net.http.ContentTypes.XML;
import static groovyx.net.http.Safe.ifClassIsLoaded;
import static groovyx.net.http.Safe.register;

public class HttpConfigs {

    public static class BasicAuth implements Auth {
        private String user;
        private String password;
        private boolean preemptive;
        private AuthType authType;

        public void basic(final String user, final String password, final boolean preemptive) {
            this.user = user;
            this.password = password;
            this.preemptive = preemptive;
            this.authType = AuthType.BASIC;
        }

        public void digest(final String user, final String password, final boolean preemptive) {
            basic(user, password, preemptive);
            this.authType = AuthType.DIGEST;
        }

        public String getUser() {
            return user;
        }

        public String getPassword() {
            return password;
        }

        public AuthType getAuthType() {
            return authType;
        }
    }

    public static class ThreadSafeAuth implements Auth {
        volatile String user;
        volatile String password;
        volatile boolean preemptive;
        volatile AuthType authType;

        public ThreadSafeAuth() { }

        public ThreadSafeAuth(final BasicAuth toCopy) {
            this.user = toCopy.user;
            this.password = toCopy.password;
            this.preemptive = toCopy.preemptive;
            this.authType = toCopy.authType;
        }

        public void basic(final String user, final String password, final boolean preemptive) {
            this.user = user;
            this.password = password;
            this.preemptive = preemptive;
            this.authType = AuthType.BASIC;
        }

        public void digest(final String user, final String password, final boolean preemptive) {
            basic(user, password, preemptive);
            this.authType = AuthType.DIGEST;
        }

        public String getUser() {
            return user;
        }

        public String getPassword() {
            return password;
        }

        public AuthType getAuthType() {
            return authType;
        }
    }

    public static abstract class BaseRequest implements ChainedRequest {

        final ChainedRequest parent;
        HttpVerb verb;

        public BaseRequest(final ChainedRequest parent) {
            this.parent = parent;
        }

        public ChainedRequest getParent() {
            return parent;
        }

        public void setCharset(final String val) {
            setCharset(Charset.forName(val));
        }

        public void setUri(final String val) {
            getUri().setFull(val);
        }

        public void setRaw(final String val){
            UriBuilder uriBuilder = getUri();
            uriBuilder.setUseRawValues(true);
            uriBuilder.setFull(val);
        }

        public void setUri(final URI val) {
            getUri().setFull(val);
        }

        public void setUri(final URL val) throws URISyntaxException {
            getUri().setFull(val.toURI());
        }

        public BiConsumer<ChainedHttpConfig,ToServer> encoder(final String contentType) {
            final BiConsumer<ChainedHttpConfig,ToServer> enc =  getEncoderMap().get(contentType);
            return enc != null ? enc : null;
        }

        public void encoder(final String contentType, final BiConsumer<ChainedHttpConfig,ToServer> val) {
            getEncoderMap().put(contentType, val);
        }

        public void encoder(final Iterable<String> contentTypes, final BiConsumer<ChainedHttpConfig,ToServer> val) {
            for(String contentType : contentTypes) {
                encoder(contentType, val);
            }
        }

        public void setAccept(final String[] values) {
            getHeaders().put("Accept", String.join(";", values));
        }

        public void setAccept(final Iterable<String> values) {
            getHeaders().put("Accept", String.join(";", values));
        }

        public void setHeaders(final Map<String,CharSequence> toAdd) {
            final Map<String,CharSequence> h = getHeaders();
            if(toAdd != null){
                for(final Map.Entry<String,CharSequence> entry : toAdd.entrySet()) {
                    h.put(entry.getKey(), entry.getValue());
                }
            }
        }

        public void cookie(final String name, final String value, final Instant instant) {
            final HttpCookie cookie = new HttpCookie(name, value);
            cookie.setPath("/");
            final Instant now = Instant.now();
            if(instant != null && now.isBefore(instant)) {
                cookie.setMaxAge(instant.getEpochSecond() - now.getEpochSecond());
            }

            getCookies().add(cookie);
        }

        public void cookie(final String name, final String value, final Date date) {
            cookie(name, value, date == null ? (Instant) null : date.toInstant());
        }

        public void cookie(final String name, final String value, final LocalDateTime dateTime){
            cookie(name, value, dateTime == null ? (Instant) null : dateTime.atZone(ZoneId.systemDefault()).toInstant());
        }

        @Override
        public HttpVerb getVerb() {
            return verb;
        }

        @Override
        public void setVerb(final HttpVerb verb) {
            this.verb = verb;
        }
    }

    public static class BasicRequest extends BaseRequest {
        private String contentType;
        private Charset charset;
        private UriBuilder uriBuilder;
        private final Map<String, CharSequence> headers = new LinkedHashMap<>();
        private Object body;
        private final Map<String,BiConsumer<ChainedHttpConfig,ToServer>> encoderMap = new LinkedHashMap<>();
        private BasicAuth auth = new BasicAuth();
        private List<HttpCookie> cookies = new ArrayList<>(1);

        protected BasicRequest(ChainedRequest parent) {
            super(parent);
            this.uriBuilder = (parent == null) ? UriBuilder.basic(null) : UriBuilder.basic(parent.getUri());
        }

        public Map<String,BiConsumer<ChainedHttpConfig,ToServer>> getEncoderMap() {
            return encoderMap;
        }

        public List<HttpCookie> getCookies() {
            return cookies;
        }

        public String getContentType() {
            return contentType;
        }

        public void setContentType(final String val) {
            this.contentType = val;
        }

        public void setCharset(final Charset val) {
            this.charset = val;
        }

        public Charset getCharset() {
            return charset;
        }

        public UriBuilder getUri() {
            return uriBuilder;
        }

        public Map<String,CharSequence> getHeaders() {
            return headers;
        }

        public Object getBody() {
            return body;
        }

        public void setBody(Object val) {
            this.body = val;
        }

        public BasicAuth getAuth() {
            return auth;
        }
    }

    public static class ThreadSafeRequest extends BaseRequest {

        private volatile String contentType;
        private volatile Charset charset;
        private volatile UriBuilder uriBuilder;
        private final ConcurrentMap<String,CharSequence> headers = new ConcurrentHashMap<>();
        private volatile Object body;
        private final ConcurrentMap<String,BiConsumer<ChainedHttpConfig,ToServer>> encoderMap = new ConcurrentHashMap<>();
        private final ThreadSafeAuth auth;
        private final List<HttpCookie> cookies = new CopyOnWriteArrayList<>();

        public ThreadSafeRequest(final ChainedRequest parent) {
            super(parent);
            this.auth = new ThreadSafeAuth();
            this.uriBuilder = (parent == null) ? UriBuilder.threadSafe(null) : UriBuilder.threadSafe(parent.getUri());
        }

        public List<HttpCookie> getCookies() {
            return cookies;
        }

        public Map<String,BiConsumer<ChainedHttpConfig,ToServer>> getEncoderMap() {
            return encoderMap;
        }

        public String getContentType() {
            return contentType;
        }

        public void setContentType(final String val) {
            this.contentType = val;
        }

        public Charset getCharset() {
            return charset;
        }

        public void setCharset(final Charset val) {
            this.charset = val;
        }

        public UriBuilder getUri() {
            return uriBuilder;
        }

        public Map<String,CharSequence> getHeaders() {
            return headers;
        }

        public Object getBody() {
            return body;
        }

        public void setBody(Object val) {
            this.body = val;
        }

        public ThreadSafeAuth getAuth() {
            return auth;
        }
    }

    public static abstract class BaseResponse implements ChainedResponse {

        abstract protected Map<Integer,BiFunction<FromServer, Object, ?>> getByCode();
        abstract protected BiFunction<FromServer, Object, ?> getSuccess();
        abstract protected BiFunction<FromServer, Object, ?> getFailure();
        abstract protected Map<String,BiFunction<ChainedHttpConfig,FromServer,Object>> getParserMap();

        private final ChainedResponse parent;

        public ChainedResponse getParent() {
            return parent;
        }

        protected BaseResponse(final ChainedResponse parent) {
            this.parent = parent;
        }

        public void when(String code, BiFunction<FromServer, Object, ?> closure) {
            when(Integer.valueOf(code), closure);
        }

        public void when(Integer code, BiFunction<FromServer, Object, ?> closure) {
            getByCode().put(code, closure);
        }

        public void when(final HttpConfig.Status status, final BiFunction<FromServer, Object, ?> closure) {
            if(status == HttpConfig.Status.SUCCESS) {
                success(closure);
            } else {
                failure(closure);
            }
        }

        public BiFunction<FromServer, Object, ?> when(final Integer code) {
            if(getByCode().containsKey(code)) {
                return getByCode().get(code);
            }

            if(code < 400 && getSuccess() != null) {
                return getSuccess();
            }

            if(code >= 400 && getFailure() != null) {
                return getFailure();
            }

            return null;
        }

        public BiFunction<ChainedHttpConfig,FromServer,Object> parser(final String contentType) {
            final BiFunction<ChainedHttpConfig,FromServer,Object> p = getParserMap().get(contentType);
            return p != null ? p : null;
        }

        public void parser(final String contentType, BiFunction<ChainedHttpConfig,FromServer,Object> val) {
            getParserMap().put(contentType, val);
        }

        public void parser(final Iterable<String> contentTypes, BiFunction<ChainedHttpConfig,FromServer,Object> val) {
            for(String contentType : contentTypes) {
                parser(contentType, val);
            }
        }

        public abstract void setType(Class<?> type);
    }

    public static class BasicResponse extends BaseResponse {

        private final Map<Integer,BiFunction<FromServer, Object, ?>> byCode = new LinkedHashMap<>();
        private BiFunction<FromServer, Object, ?> successHandler;
        private BiFunction<FromServer, Object, ?> failureHandler;
        private Function<Throwable,?> exceptionHandler;
        private final Map<String,BiFunction<ChainedHttpConfig,FromServer,Object>> parserMap = new LinkedHashMap<>();
        private Class<?> type = Object.class;

        protected BasicResponse(final ChainedResponse parent) {
            super(parent);
        }

        public Map<String,BiFunction<ChainedHttpConfig,FromServer,Object>> getParserMap() {
            return parserMap;
        }

        protected Map<Integer,BiFunction<FromServer, Object, ?>> getByCode() {
            return byCode;
        }

        protected BiFunction<FromServer, Object, ?> getSuccess() {
            return successHandler;
        }

        protected BiFunction<FromServer, Object, ?> getFailure() {
            return failureHandler;
        }

        public Function<Throwable,?> getException() {
            return exceptionHandler;
        }

        public void success(final BiFunction<FromServer, Object, ?> val) {
            successHandler = val;
        }

        public void failure(final BiFunction<FromServer, Object, ?> val) {
            failureHandler = val;
        }

        public void exception(final Function<Throwable,?> val) {
            exceptionHandler = val;
        }

        public Class<?> getType() {
            return type;
        }

        public void setType(Class<?> val) {
            type = val;
        }
    }

    public static class ThreadSafeResponse extends BaseResponse {

        private final ConcurrentMap<String,BiFunction<ChainedHttpConfig,FromServer,Object>> parserMap = new ConcurrentHashMap<>();
        private final ConcurrentMap<Integer,BiFunction<FromServer, Object, ?>> byCode = new ConcurrentHashMap<>();
        private volatile BiFunction<FromServer, Object, ?> successHandler;
        private volatile BiFunction<FromServer, Object, ?> failureHandler;
        private volatile Function<Throwable,?> exceptionHandler;
        private volatile Class<?> type = Object.class;

        public ThreadSafeResponse(final ChainedResponse parent) {
            super(parent);
        }

        protected Map<String,BiFunction<ChainedHttpConfig,FromServer,Object>> getParserMap() {
            return parserMap;
        }

        protected Map<Integer,BiFunction<FromServer, Object, ?>> getByCode() {
            return byCode;
        }

        protected BiFunction<FromServer, Object, ?> getSuccess() {
            return successHandler;
        }

        protected BiFunction<FromServer, Object, ?> getFailure() {
            return failureHandler;
        }

        public Function<Throwable,?> getException() {
            return exceptionHandler;
        }

        public void success(final BiFunction<FromServer, Object, ?> val) {
            successHandler = val;
        }

        public void failure(final BiFunction<FromServer, Object, ?> val) {
            failureHandler = val;
        }

        public void exception(final Function<Throwable,?> val) {
            this.exceptionHandler = val;
        }

        public Class<?> getType() {
            return type;
        }

        public void setType(final Class<?> val) {
            type = val;
        }
    }

    public abstract static class BaseHttpConfig implements ChainedHttpConfig {

        private final ChainedHttpConfig parent;

        public BaseHttpConfig(ChainedHttpConfig parent) {
            this.parent = parent;
        }

        public ChainedHttpConfig getParent() {
            return parent;
        }

        public ChainedHttpConfig configure() {
            getRequest().setCharset(StandardCharsets.UTF_8);
            getRequest().encoder(BINARY, NativeHandlers.Encoders::binary);
            getRequest().encoder(TEXT, (f,s) -> {
                    try {
                        NativeHandlers.Encoders.text(f, s);
                    }
                    catch(IOException e) {
                        throw new TransportingException(e);
                    } });

            getRequest().encoder(URLENC, NativeHandlers.Encoders::form);
            getRequest().encoder(XML, NativeHandlers.Encoders::xml);
            getRequest().encoder(JSON, NativeHandlers.Encoders::json);

            getResponse().success(NativeHandlers::success);
            getResponse().failure(NativeHandlers::failure);
            getResponse().exception(NativeHandlers::exception);

            getResponse().parser(BINARY, NativeHandlers.Parsers::streamToBytes);
            getResponse().parser(TEXT, NativeHandlers.Parsers::textToString);
            getResponse().parser(URLENC, NativeHandlers.Parsers::form);
            getResponse().parser(XML, NativeHandlers.Parsers::xml);
            getResponse().parser(JSON, NativeHandlers.Parsers::json);

            return this;
        }

        public void context(final String contentType, final Object id, final Object obj) {
            getContextMap().put(new AbstractMap.SimpleImmutableEntry<>(contentType, id), obj);
        }
    }

    public static class ThreadSafeHttpConfig extends BaseHttpConfig {
        private final ThreadSafeRequest request;
        private final ThreadSafeResponse response;
        private final ConcurrentMap<Map.Entry<String,Object>,Object> contextMap = new ConcurrentHashMap<>();

        public ThreadSafeHttpConfig(final ChainedHttpConfig parent) {
            super(parent);
            if(parent == null) {
                this.request = new ThreadSafeRequest(null);
                this.response = new ThreadSafeResponse(null);
            }
            else {
                this.request = new ThreadSafeRequest(parent.getChainedRequest());
                this.response = new ThreadSafeResponse(parent.getChainedResponse());
            }
        }

        public Request getRequest() {
            return request;
        }

        public Response getResponse() {
            return response;
        }

        public ChainedRequest getChainedRequest() {
            return request;
        }

        public ChainedResponse getChainedResponse() {
            return response;
        }

        public Map<Map.Entry<String,Object>,Object> getContextMap() {
            return contextMap;
        }
    }

    public static class BasicHttpConfig extends BaseHttpConfig {
        private final BasicRequest request;
        private final BasicResponse response;
        private final Map<Map.Entry<String,Object>,Object> contextMap = new LinkedHashMap<>(1);

        public BasicHttpConfig(final ChainedHttpConfig parent) {
            super(parent);
            if(parent == null) {
                this.request = new BasicRequest(null);
                this.response = new BasicResponse(null);
            }
            else {
                this.request = new BasicRequest(parent.getChainedRequest());
                this.response = new BasicResponse(parent.getChainedResponse());
            }
        }

        public BasicRequest getRequest() {
            return request;
        }

        public BasicResponse getResponse() {
            return response;
        }

        public BasicRequest getChainedRequest() {
            return request;
        }

        public BasicResponse getChainedResponse() {
            return response;
        }

        public Map<Map.Entry<String,Object>,Object> getContextMap() {
            return contextMap;
        }
    }

    private static final ThreadSafeHttpConfig root;

    static {
        root = (ThreadSafeHttpConfig) new ThreadSafeHttpConfig(null).configure();

        register(root, ifClassIsLoaded("org.cyberneko.html.parsers.SAXParser"),
                 "text/html", () -> NativeHandlers.Encoders::xml, Html.neckoParserSupplier);

        register(root, ifClassIsLoaded("org.jsoup.Jsoup"),
                 "text/html", Html.jsoupEncoderSupplier, Html.jsoupParserSupplier);

        if(register(root, ifClassIsLoaded("com.opencsv.CSVReader"),
                    "text/csv", Csv.encoderSupplier, Csv.parserSupplier)) {
            root.context("text/csv", Csv.Context.ID, Csv.Context.DEFAULT_CSV);
        }

        if(register(root, ifClassIsLoaded("com.opencsv.CSVReader"),
                    "text/tab-separated-values", Csv.encoderSupplier, Csv.parserSupplier)) {
            root.context("text/tab-separated-values", Csv.Context.ID, Csv.Context.DEFAULT_TSV);
        }
    }

    public static ChainedHttpConfig root() {
        return root;
    }

    public static ChainedHttpConfig threadSafe(final ChainedHttpConfig parent) {
        return new ThreadSafeHttpConfig(parent);
    }

    public static ChainedHttpConfig classLevel(final boolean threadSafe) {
        return threadSafe ? threadSafe(root) : basic(root);
    }

    public static ChainedHttpConfig basic(final ChainedHttpConfig parent) {
        return new BasicHttpConfig(parent);
    }

    public static BasicHttpConfig requestLevel(final ChainedHttpConfig parent) {
        return new BasicHttpConfig(parent);
    }
}