org.jclouds.rest.internal.RestAnnotationProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.jclouds.rest.internal.RestAnnotationProcessor.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.jclouds.rest.internal;

import static com.google.common.base.Functions.toStringFunction;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Predicates.instanceOf;
import static com.google.common.collect.Collections2.filter;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.get;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Iterables.tryFind;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newLinkedList;
import static com.google.common.collect.Multimaps.transformValues;
import static com.google.common.net.HttpHeaders.ACCEPT;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.HOST;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static org.jclouds.http.HttpUtils.filterOutContentHeaders;
import static org.jclouds.http.HttpUtils.tryFindHttpMethod;
import static org.jclouds.http.Uris.uriBuilder;
import static org.jclouds.io.Payloads.newPayload;
import static org.jclouds.util.Strings2.replaceTokens;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.net.URI;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;

import javax.annotation.Resource;
import javax.inject.Named;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.jclouds.Constants;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpRequestFilter;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.Uris.UriBuilder;
import org.jclouds.http.options.HttpRequestOptions;
import org.jclouds.io.ContentMetadataCodec;
import org.jclouds.io.Payload;
import org.jclouds.io.PayloadEnclosing;
import org.jclouds.io.Payloads;
import org.jclouds.io.payloads.MultipartForm;
import org.jclouds.io.payloads.Part;
import org.jclouds.io.payloads.Part.PartOptions;
import org.jclouds.javax.annotation.Nullable;
import org.jclouds.logging.Logger;
import org.jclouds.reflect.Invocation;
import org.jclouds.rest.Binder;
import org.jclouds.rest.InputParamValidator;
import org.jclouds.rest.annotations.ApiVersion;
import org.jclouds.rest.annotations.BinderParam;
import org.jclouds.rest.annotations.BuildVersion;
import org.jclouds.rest.annotations.Endpoint;
import org.jclouds.rest.annotations.EndpointParam;
import org.jclouds.rest.annotations.FormParams;
import org.jclouds.rest.annotations.Headers;
import org.jclouds.rest.annotations.MapBinder;
import org.jclouds.rest.annotations.OverrideRequestFilters;
import org.jclouds.rest.annotations.ParamParser;
import org.jclouds.rest.annotations.PartParam;
import org.jclouds.rest.annotations.PayloadParam;
import org.jclouds.rest.annotations.PayloadParams;
import org.jclouds.rest.annotations.QueryParams;
import org.jclouds.rest.annotations.RequestFilters;
import org.jclouds.rest.annotations.SkipEncoding;
import org.jclouds.rest.annotations.VirtualHost;
import org.jclouds.rest.annotations.WrapWith;
import org.jclouds.rest.binders.BindMapToStringPayload;
import org.jclouds.rest.binders.BindToJsonPayloadWrappedWith;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.primitives.Chars;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.Parameter;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;

/**
 * 
 * @author Adrian Cole
 */
public class RestAnnotationProcessor implements Function<Invocation, HttpRequest> {

    @Resource
    protected Logger logger = Logger.NULL;

    private static final Function<? super Entry<String, Object>, ? extends Part> ENTRY_TO_PART = new Function<Entry<String, Object>, Part>() {
        @Override
        public Part apply(Entry<String, Object> from) {
            return Part.create(from.getKey(), from.getValue().toString());
        }
    };

    private final Injector injector;
    private final HttpUtils utils;
    private final ContentMetadataCodec contentMetadataCodec;
    private final String apiVersion;
    private final String buildVersion;
    private final InputParamValidator inputParamValidator;
    private final GetAcceptHeaders getAcceptHeaders;
    private final Invocation caller;

    @Inject
    private RestAnnotationProcessor(Injector injector, @ApiVersion String apiVersion,
            @BuildVersion String buildVersion, HttpUtils utils, ContentMetadataCodec contentMetadataCodec,
            InputParamValidator inputParamValidator, GetAcceptHeaders getAcceptHeaders,
            @Nullable @Named("caller") Invocation caller) {
        this.injector = injector;
        this.utils = utils;
        this.contentMetadataCodec = contentMetadataCodec;
        this.apiVersion = apiVersion;
        this.buildVersion = buildVersion;
        this.inputParamValidator = inputParamValidator;
        this.getAcceptHeaders = getAcceptHeaders;
        this.caller = caller;
    }

    /**
     * Note this is dangerous as it cannot pass the inheriting class! Using this
     * when subclassing interfaces may result in lost data.
     */
    @Deprecated
    public GeneratedHttpRequest createRequest(Invokable<?, ?> invokable, List<Object> args) {
        return apply(Invocation.create(invokable, args));
    }

    @Override
    public GeneratedHttpRequest apply(Invocation invocation) {
        checkNotNull(invocation, "invocation");
        inputParamValidator.validateMethodParametersOrThrow(invocation);

        Optional<URI> endpoint = Optional.absent();
        HttpRequest r = findOrNull(invocation.getArgs(), HttpRequest.class);
        if (r != null) {
            endpoint = Optional.fromNullable(r.getEndpoint());
            if (endpoint.isPresent())
                logger.trace("using endpoint %s from invocation.getArgs() for %s", endpoint, invocation);
        } else if (caller != null) {
            endpoint = getEndpointFor(caller);
            if (endpoint.isPresent())
                logger.trace("using endpoint %s from caller %s for %s", endpoint, caller, invocation);
            else
                endpoint = findEndpoint(invocation);
        } else {
            endpoint = findEndpoint(invocation);
        }

        if (!endpoint.isPresent())
            throw new NoSuchElementException(format("no endpoint found for %s", invocation));
        GeneratedHttpRequest.Builder requestBuilder = GeneratedHttpRequest.builder().invocation(invocation)
                .caller(caller);
        String requestMethod = null;
        if (r != null) {
            requestMethod = r.getMethod();
            requestBuilder.fromHttpRequest(r);
        } else {
            requestMethod = tryFindHttpMethod(invocation.getInvokable()).get();
            requestBuilder.method(requestMethod);
        }

        requestBuilder.filters(getFiltersIfAnnotated(invocation));

        Multimap<String, Object> tokenValues = LinkedHashMultimap.create();

        tokenValues.put(Constants.PROPERTY_API_VERSION, apiVersion);
        tokenValues.put(Constants.PROPERTY_BUILD_VERSION, buildVersion);
        // URI template in rfc6570 form
        UriBuilder uriBuilder = uriBuilder(endpoint.get().toString());

        overridePathEncoding(uriBuilder, invocation);

        if (caller != null)
            tokenValues.putAll(addPathAndGetTokens(caller, uriBuilder));
        tokenValues.putAll(addPathAndGetTokens(invocation, uriBuilder));
        Multimap<String, Object> formParams;
        if (caller != null) {
            formParams = addFormParams(tokenValues, caller);
            formParams.putAll(addFormParams(tokenValues, invocation));
        } else {
            formParams = addFormParams(tokenValues, invocation);
        }
        Multimap<String, Object> queryParams = addQueryParams(tokenValues, invocation);
        Multimap<String, String> headers = buildHeaders(tokenValues, invocation);

        if (r != null)
            headers.putAll(r.getHeaders());

        if (shouldAddHostHeader(invocation)) {
            StringBuilder hostHeader = new StringBuilder(endpoint.get().getHost());
            if (endpoint.get().getPort() != -1)
                hostHeader.append(":").append(endpoint.get().getPort());
            headers.put(HOST, hostHeader.toString());
        }

        Payload payload = null;
        for (HttpRequestOptions options : findOptionsIn(invocation)) {
            injector.injectMembers(options);// TODO test case
            for (Entry<String, String> header : options.buildRequestHeaders().entries()) {
                headers.put(header.getKey(), replaceTokens(header.getValue(), tokenValues));
            }
            for (Entry<String, String> query : options.buildQueryParameters().entries()) {
                queryParams.put(query.getKey(), replaceTokens(query.getValue(), tokenValues));
            }
            for (Entry<String, String> form : options.buildFormParameters().entries()) {
                formParams.put(form.getKey(), replaceTokens(form.getValue(), tokenValues));
            }

            String pathSuffix = options.buildPathSuffix();
            if (pathSuffix != null) {
                uriBuilder.appendPath(pathSuffix);
            }
            String stringPayload = options.buildStringPayload();
            if (stringPayload != null)
                payload = Payloads.newStringPayload(stringPayload);
        }

        if (queryParams.size() > 0) {
            uriBuilder.query(queryParams);
        }

        requestBuilder.headers(filterOutContentHeaders(headers));

        requestBuilder.endpoint(uriBuilder.build(convertUnsafe(tokenValues)));

        if (payload == null) {
            PayloadEnclosing payloadEnclosing = findOrNull(invocation.getArgs(), PayloadEnclosing.class);
            payload = (payloadEnclosing != null) ? payloadEnclosing.getPayload()
                    : findOrNull(invocation.getArgs(), Payload.class);
        }

        List<? extends Part> parts = getParts(invocation,
                ImmutableMultimap.<String, Object>builder().putAll(tokenValues).putAll(formParams).build());

        if (parts.size() > 0) {
            if (formParams.size() > 0) {
                parts = newLinkedList(concat(transform(formParams.entries(), ENTRY_TO_PART), parts));
            }
            payload = new MultipartForm(MultipartForm.BOUNDARY, parts);
        } else if (formParams.size() > 0) {
            payload = Payloads
                    .newUrlEncodedFormPayload(transformValues(formParams, NullableToStringFunction.INSTANCE));
        } else if (headers.containsKey(CONTENT_TYPE) && !HttpRequest.NON_PAYLOAD_METHODS.contains(requestMethod)) {
            if (payload == null)
                payload = Payloads.newByteArrayPayload(new byte[] {});
            payload.getContentMetadata().setContentType(get(headers.get(CONTENT_TYPE), 0));
        }
        if (payload != null) {
            requestBuilder.payload(payload);
        }
        GeneratedHttpRequest request = requestBuilder.build();

        org.jclouds.rest.MapBinder mapBinder = getMapPayloadBinderOrNull(invocation);
        if (mapBinder != null) {
            Map<String, Object> mapParams;
            if (caller != null) {
                mapParams = buildPayloadParams(caller);
                mapParams.putAll(buildPayloadParams(invocation));
            } else {
                mapParams = buildPayloadParams(invocation);
            }
            if (invocation.getInvokable().isAnnotationPresent(PayloadParams.class)) {
                PayloadParams params = invocation.getInvokable().getAnnotation(PayloadParams.class);
                addMapPayload(mapParams, params, headers);
            }
            request = mapBinder.bindToRequest(request, mapParams);
        } else {
            request = decorateRequest(request);
        }

        if (request.getPayload() != null) {
            contentMetadataCodec.fromHeaders(request.getPayload().getContentMetadata(), headers);
        }
        utils.checkRequestHasRequiredProperties(request);
        return request;
    }

    private static <T> T findOrNull(Iterable<Object> args, Class<T> clazz) {
        return clazz.cast(tryFind(args, instanceOf(clazz)).orNull());
    }

    private static <K, V> Map<K, V> convertUnsafe(Multimap<K, V> in) {
        LinkedHashMap<K, V> out = Maps.newLinkedHashMap();
        for (Entry<K, V> entry : in.entries()) {
            out.put(entry.getKey(), entry.getValue());
        }
        return ImmutableMap.copyOf(out);
    }

    private void overridePathEncoding(UriBuilder uriBuilder, Invocation invocation) {
        if (invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(SkipEncoding.class)) {
            uriBuilder.skipPathEncoding(Chars.asList(invocation.getInvokable().getOwnerType().getRawType()
                    .getAnnotation(SkipEncoding.class).value()));
        }
        if (invocation.getInvokable().isAnnotationPresent(SkipEncoding.class)) {
            uriBuilder.skipPathEncoding(
                    Chars.asList(invocation.getInvokable().getAnnotation(SkipEncoding.class).value()));
        }
    }

    // different than guava as accepts null
    private static enum NullableToStringFunction implements Function<Object, String> {
        INSTANCE;
        @Override
        public String apply(Object o) {
            if (o == null)
                return null;
            return o.toString();
        }
    }

    protected Optional<URI> findEndpoint(Invocation invocation) {
        Optional<URI> endpoint = getEndpointFor(invocation);
        if (endpoint.isPresent())
            logger.trace("using endpoint %s for %s", endpoint, invocation);
        if (!endpoint.isPresent()) {
            logger.trace("looking up default endpoint for %s", invocation);
            endpoint = Optional.fromNullable(
                    injector.getInstance(Key.get(uriSupplierLiteral, org.jclouds.location.Provider.class)).get());
            if (endpoint.isPresent())
                logger.trace("using default endpoint %s for %s", endpoint, invocation);
        }
        return endpoint;
    }

    private Multimap<String, Object> addPathAndGetTokens(Invocation invocation, UriBuilder uriBuilder) {
        if (invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(Path.class))
            uriBuilder.appendPath(
                    invocation.getInvokable().getOwnerType().getRawType().getAnnotation(Path.class).value());
        if (invocation.getInvokable().isAnnotationPresent(Path.class))
            uriBuilder.appendPath(invocation.getInvokable().getAnnotation(Path.class).value());
        return getPathParamKeyValues(invocation);
    }

    private Multimap<String, Object> addFormParams(Multimap<String, ?> tokenValues, Invocation invocation) {
        Multimap<String, Object> formMap = LinkedListMultimap.create();
        if (invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(FormParams.class)) {
            FormParams form = invocation.getInvokable().getOwnerType().getRawType().getAnnotation(FormParams.class);
            addForm(formMap, form, tokenValues);
        }

        if (invocation.getInvokable().isAnnotationPresent(FormParams.class)) {
            FormParams form = invocation.getInvokable().getAnnotation(FormParams.class);
            addForm(formMap, form, tokenValues);
        }

        for (Entry<String, Object> form : getFormParamKeyValues(invocation).entries()) {
            formMap.put(form.getKey(), replaceTokens(form.getValue().toString(), tokenValues));
        }
        return formMap;
    }

    private Multimap<String, Object> addQueryParams(Multimap<String, ?> tokenValues, Invocation invocation) {
        Multimap<String, Object> queryMap = LinkedListMultimap.create();
        if (invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(QueryParams.class)) {
            QueryParams query = invocation.getInvokable().getOwnerType().getRawType()
                    .getAnnotation(QueryParams.class);
            addQuery(queryMap, query, tokenValues);
        }

        if (invocation.getInvokable().isAnnotationPresent(QueryParams.class)) {
            QueryParams query = invocation.getInvokable().getAnnotation(QueryParams.class);
            addQuery(queryMap, query, tokenValues);
        }

        for (Entry<String, Object> query : getQueryParamKeyValues(invocation).entries()) {
            queryMap.put(query.getKey(), replaceTokens(query.getValue().toString(), tokenValues));
        }
        return queryMap;
    }

    private void addForm(Multimap<String, Object> formParams, FormParams form, Multimap<String, ?> tokenValues) {
        for (int i = 0; i < form.keys().length; i++) {
            if (form.values()[i].equals(FormParams.NULL)) {
                formParams.removeAll(form.keys()[i]);
                formParams.put(form.keys()[i], null);
            } else {
                formParams.put(form.keys()[i], replaceTokens(form.values()[i], tokenValues));
            }
        }
    }

    private void addQuery(Multimap<String, Object> queryParams, QueryParams query,
            Multimap<String, ?> tokenValues) {
        for (int i = 0; i < query.keys().length; i++) {
            if (query.values()[i].equals(QueryParams.NULL)) {
                queryParams.removeAll(query.keys()[i]);
                queryParams.put(query.keys()[i], null);
            } else {
                queryParams.put(query.keys()[i], replaceTokens(query.values()[i], tokenValues));
            }
        }
    }

    private void addMapPayload(Map<String, Object> postParams, PayloadParams mapDefaults,
            Multimap<String, String> headers) {
        for (int i = 0; i < mapDefaults.keys().length; i++) {
            if (mapDefaults.values()[i].equals(PayloadParams.NULL)) {
                postParams.put(mapDefaults.keys()[i], null);
            } else {
                postParams.put(mapDefaults.keys()[i], replaceTokens(mapDefaults.values()[i], headers));
            }
        }
    }

    private List<HttpRequestFilter> getFiltersIfAnnotated(Invocation invocation) {
        List<HttpRequestFilter> filters = newArrayList();
        if (invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(RequestFilters.class)) {
            for (Class<? extends HttpRequestFilter> clazz : invocation.getInvokable().getOwnerType().getRawType()
                    .getAnnotation(RequestFilters.class).value()) {
                HttpRequestFilter instance = injector.getInstance(clazz);
                filters.add(instance);
                logger.trace("adding filter %s from annotation on %s", instance,
                        invocation.getInvokable().getOwnerType().getRawType().getName());
            }
        }
        if (invocation.getInvokable().isAnnotationPresent(RequestFilters.class)) {
            if (invocation.getInvokable().isAnnotationPresent(OverrideRequestFilters.class))
                filters.clear();
            for (Class<? extends HttpRequestFilter> clazz : invocation.getInvokable()
                    .getAnnotation(RequestFilters.class).value()) {
                HttpRequestFilter instance = injector.getInstance(clazz);
                filters.add(instance);
                logger.trace("adding filter %s from annotation on %s", instance,
                        invocation.getInvokable().getName());
            }
        }
        return filters;
    }

    @VisibleForTesting
    static URI getEndpointInParametersOrNull(Invocation invocation, Injector injector) {
        Collection<Parameter> endpointParams = parametersWithAnnotation(invocation.getInvokable(),
                EndpointParam.class);
        if (endpointParams.isEmpty())
            return null;
        checkState(endpointParams.size() == 1, "invocation.getInvoked() %s has too many EndpointParam annotations",
                invocation.getInvokable());
        Parameter endpointParam = get(endpointParams, 0);
        Function<Object, URI> parser = injector
                .getInstance(endpointParam.getAnnotation(EndpointParam.class).parser());
        int position = endpointParam.hashCode();// guava issue 1243
        try {
            URI returnVal = parser.apply(invocation.getArgs().get(position));
            checkArgument(returnVal != null,
                    format("endpoint for [%s] not configured for %s", position, invocation.getInvokable()));
            return returnVal;
        } catch (NullPointerException e) {
            throw new IllegalArgumentException(format("argument at index %d on invocation.getInvoked() %s was null",
                    position, invocation.getInvokable()), e);
        }
    }

    private static Collection<Parameter> parametersWithAnnotation(Invokable<?, ?> invokable,
            final Class<? extends Annotation> annotationType) {
        return filter(invokable.getParameters(), new Predicate<Parameter>() {
            public boolean apply(Parameter in) {
                return in.isAnnotationPresent(annotationType);
            }
        });
    }

    private static final TypeLiteral<Supplier<URI>> uriSupplierLiteral = new TypeLiteral<Supplier<URI>>() {
    };

    protected Optional<URI> getEndpointFor(Invocation invocation) {
        URI endpoint = getEndpointInParametersOrNull(invocation, injector);
        if (endpoint == null) {
            Endpoint annotation;
            if (invocation.getInvokable().isAnnotationPresent(Endpoint.class)) {
                annotation = invocation.getInvokable().getAnnotation(Endpoint.class);
            } else if (invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(Endpoint.class)) {
                annotation = invocation.getInvokable().getOwnerType().getRawType().getAnnotation(Endpoint.class);
            } else {
                logger.trace("no annotations on class or invocation.getInvoked(): %s", invocation.getInvokable());
                return Optional.absent();
            }
            endpoint = injector.getInstance(Key.get(uriSupplierLiteral, annotation.value())).get();
        }
        URI provider = injector.getInstance(Key.get(uriSupplierLiteral, org.jclouds.location.Provider.class)).get();
        return Optional.fromNullable(addHostIfMissing(endpoint, provider));
    }

    @VisibleForTesting
    static URI addHostIfMissing(URI original, URI withHost) {
        checkNotNull(withHost, "URI withHost cannot be null");
        checkArgument(withHost.getHost() != null, "URI withHost must have host:" + withHost);
        if (original == null)
            return null;
        if (original.getHost() != null)
            return original;
        return withHost.resolve(original);
    }

    private org.jclouds.rest.MapBinder getMapPayloadBinderOrNull(Invocation invocation) {
        if (invocation.getArgs() != null) {
            for (Object arg : invocation.getArgs()) {
                if (arg instanceof Object[]) {
                    Object[] postBinders = (Object[]) arg;
                    if (postBinders.length == 0) {
                    } else if (postBinders.length == 1) {
                        if (postBinders[0] instanceof org.jclouds.rest.MapBinder) {
                            org.jclouds.rest.MapBinder binder = (org.jclouds.rest.MapBinder) postBinders[0];
                            injector.injectMembers(binder);
                            return binder;
                        }
                    } else {
                        if (postBinders[0] instanceof org.jclouds.rest.MapBinder) {
                            throw new IllegalArgumentException(
                                    "we currently do not support multiple varinvocation.getArgs() postBinders in: "
                                            + invocation.getInvokable().getName());
                        }
                    }
                } else if (arg instanceof org.jclouds.rest.MapBinder) {
                    org.jclouds.rest.MapBinder binder = (org.jclouds.rest.MapBinder) arg;
                    injector.injectMembers(binder);
                    return binder;
                }
            }
        }
        if (invocation.getInvokable().isAnnotationPresent(MapBinder.class)) {
            return injector.getInstance(invocation.getInvokable().getAnnotation(MapBinder.class).value());
        } else if (invocation.getInvokable().isAnnotationPresent(org.jclouds.rest.annotations.Payload.class)) {
            return injector.getInstance(BindMapToStringPayload.class);
        } else if (invocation.getInvokable().isAnnotationPresent(WrapWith.class)) {
            return injector.getInstance(BindToJsonPayloadWrappedWith.Factory.class)
                    .create(invocation.getInvokable().getAnnotation(WrapWith.class).value());
        }
        return null;
    }

    private boolean shouldAddHostHeader(Invocation invocation) {
        return invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(VirtualHost.class)
                || invocation.getInvokable().isAnnotationPresent(VirtualHost.class);
    }

    private GeneratedHttpRequest decorateRequest(GeneratedHttpRequest request) throws NegativeArraySizeException {
        Invocation invocation = request.getInvocation();
        List<Object> args = request.getInvocation().getArgs();
        Set<Parameter> binderOrWrapWith = ImmutableSet
                .copyOf(concat(parametersWithAnnotation(invocation.getInvokable(), BinderParam.class),
                        parametersWithAnnotation(invocation.getInvokable(), WrapWith.class)));
        OUTER: for (Parameter entry : binderOrWrapWith) {
            int position = entry.hashCode();
            boolean shouldBreak = false;
            Binder binder;
            if (entry.isAnnotationPresent(BinderParam.class))
                binder = injector.getInstance(entry.getAnnotation(BinderParam.class).value());
            else
                binder = injector.getInstance(BindToJsonPayloadWrappedWith.Factory.class)
                        .create(entry.getAnnotation(WrapWith.class).value());
            Object arg = args.size() >= position + 1 ? args.get(position) : null;
            if (args.size() >= position + 1 && arg != null) {
                Class<?> parameterType = entry.getType().getRawType();
                Class<? extends Object> argType = arg.getClass();
                if (!argType.isArray() && parameterType.isArray()) {// TODO: &&
                                                                    // invocation.getInvokable().isVarArgs())
                                                                    // {
                    int arrayLength = args.size() - invocation.getInvokable().getParameters().size() + 1;
                    if (arrayLength == 0)
                        break OUTER;
                    arg = (Object[]) Array.newInstance(arg.getClass(), arrayLength);
                    System.arraycopy(args.toArray(), position, arg, 0, arrayLength);
                    shouldBreak = true;
                } else if (argType.isArray() && parameterType.isArray()) {// TODO:
                                                                          // &&
                                                                          // invocation.getInvokable().isVarArgs())
                                                                          // {
                } else {
                    if (arg.getClass().isArray()) {
                        Object[] payloadArray = (Object[]) arg;
                        arg = payloadArray.length > 0 ? payloadArray[0] : null;
                    }
                }
                if (arg != null) {
                    request = binder.bindToRequest(request, arg);
                }
                if (shouldBreak)
                    break OUTER;
            } else {
                if (position + 1 == invocation.getInvokable().getParameters().size() && entry.getType().isArray())// TODO:
                                                                                                                  // &&
                                                                                                                  // invocation.getInvokable().isVarArgs())
                    continue OUTER;

                if (entry.isAnnotationPresent(Nullable.class)) {
                    continue OUTER;
                }
                checkNotNull(arg, invocation.getInvokable().getName() + " parameter " + (position + 1));
            }
        }
        return request;
    }

    private static final LoadingCache<Invokable<?, ?>, Set<Integer>> invokableToIndexesOfOptions = CacheBuilder
            .newBuilder().build(new CacheLoader<Invokable<?, ?>, Set<Integer>>() {
                @Override
                public Set<Integer> load(Invokable<?, ?> invokable) {
                    Builder<Integer> toReturn = ImmutableSet.builder();
                    for (Parameter param : invokable.getParameters()) {
                        Class<?> type = param.getType().getRawType();
                        if (HttpRequestOptions.class.isAssignableFrom(type)
                                || HttpRequestOptions[].class.isAssignableFrom(type))
                            toReturn.add(param.hashCode());
                    }
                    return toReturn.build();
                }
            });

    private Set<HttpRequestOptions> findOptionsIn(Invocation invocation) {
        ImmutableSet.Builder<HttpRequestOptions> result = ImmutableSet.builder();
        for (int index : invokableToIndexesOfOptions.getUnchecked(invocation.getInvokable())) {
            if (invocation.getArgs().size() >= index + 1) {// accommodate
                                                           // varinvocation.getArgs()
                if (invocation.getArgs().get(index) instanceof Object[]) {
                    for (Object option : (Object[]) invocation.getArgs().get(index)) {
                        if (option instanceof HttpRequestOptions) {
                            result.add((HttpRequestOptions) option);
                        }
                    }
                } else {
                    for (; index < invocation.getArgs().size(); index++) {
                        if (invocation.getArgs().get(index) instanceof HttpRequestOptions) {
                            result.add((HttpRequestOptions) invocation.getArgs().get(index));
                        }
                    }
                }
            }
        }
        return result.build();
    }

    private Multimap<String, String> buildHeaders(Multimap<String, ?> tokenValues, Invocation invocation) {
        Multimap<String, String> headers = LinkedHashMultimap.create();
        addHeaderIfAnnotationPresentOnMethod(headers, invocation, tokenValues);
        for (Parameter headerParam : parametersWithAnnotation(invocation.getInvokable(), HeaderParam.class)) {
            Annotation key = headerParam.getAnnotation(HeaderParam.class);
            String value = invocation.getArgs().get(headerParam.hashCode()).toString();
            value = replaceTokens(value, tokenValues);
            headers.put(((HeaderParam) key).value(), value);
        }
        addProducesIfPresentOnTypeOrMethod(headers, invocation);
        addConsumesIfPresentOnTypeOrMethod(headers, invocation);
        return headers;
    }

    private void addConsumesIfPresentOnTypeOrMethod(Multimap<String, String> headers, Invocation invocation) {
        Set<String> accept = getAcceptHeaders.apply(invocation);
        if (!accept.isEmpty())
            headers.replaceValues(ACCEPT, accept);
    }

    private void addProducesIfPresentOnTypeOrMethod(Multimap<String, String> headers, Invocation invocation) {
        if (invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(Produces.class)) {
            Produces header = invocation.getInvokable().getOwnerType().getRawType().getAnnotation(Produces.class);
            headers.replaceValues(CONTENT_TYPE, asList(header.value()));
        }
        if (invocation.getInvokable().isAnnotationPresent(Produces.class)) {
            Produces header = invocation.getInvokable().getAnnotation(Produces.class);
            headers.replaceValues(CONTENT_TYPE, asList(header.value()));
        }
    }

    private void addHeaderIfAnnotationPresentOnMethod(Multimap<String, String> headers, Invocation invocation,
            Multimap<String, ?> tokenValues) {
        if (invocation.getInvokable().getOwnerType().getRawType().isAnnotationPresent(Headers.class)) {
            Headers header = invocation.getInvokable().getOwnerType().getRawType().getAnnotation(Headers.class);
            addHeader(headers, header, tokenValues);
        }
        if (invocation.getInvokable().isAnnotationPresent(Headers.class)) {
            Headers header = invocation.getInvokable().getAnnotation(Headers.class);
            addHeader(headers, header, tokenValues);
        }
    }

    private static void addHeader(Multimap<String, String> headers, Headers header,
            Multimap<String, ?> tokenValues) {
        for (int i = 0; i < header.keys().length; i++) {
            String value = header.values()[i];
            value = replaceTokens(value, tokenValues);
            headers.put(header.keys()[i], value);
        }
    }

    private static List<Part> getParts(Invocation invocation, Multimap<String, ?> tokenValues) {
        ImmutableList.Builder<Part> parts = ImmutableList.<Part>builder();
        for (Parameter param : parametersWithAnnotation(invocation.getInvokable(), PartParam.class)) {
            PartParam partParam = param.getAnnotation(PartParam.class);
            PartOptions options = new PartOptions();
            if (!PartParam.NO_CONTENT_TYPE.equals(partParam.contentType()))
                options.contentType(partParam.contentType());
            if (!PartParam.NO_FILENAME.equals(partParam.filename()))
                options.filename(replaceTokens(partParam.filename(), tokenValues));
            Object arg = invocation.getArgs().get(param.hashCode());
            checkNotNull(arg, partParam.name());
            Part part = Part.create(partParam.name(), newPayload(arg), options);
            parts.add(part);
        }
        return parts.build();
    }

    private Multimap<String, Object> getPathParamKeyValues(Invocation invocation) {
        Multimap<String, Object> pathParamValues = LinkedHashMultimap.create();
        for (Parameter param : parametersWithAnnotation(invocation.getInvokable(), PathParam.class)) {
            PathParam pathParam = param.getAnnotation(PathParam.class);
            String paramKey = pathParam.value();
            Optional<?> paramValue = getParamValue(invocation, param.getAnnotation(ParamParser.class),
                    param.hashCode(), paramKey);
            if (paramValue.isPresent())
                pathParamValues.put(paramKey, paramValue.get().toString());
        }
        return pathParamValues;
    }

    private Optional<?> getParamValue(Invocation invocation, @Nullable ParamParser extractor, int argIndex,
            String paramKey) {
        Object arg = invocation.getArgs().get(argIndex);
        if (extractor != null && checkPresentOrNullable(invocation, paramKey, argIndex, arg)) {
            // ParamParsers can deal with nullable parameters
            arg = injector.getInstance(extractor.value()).apply(arg);
        }
        checkPresentOrNullable(invocation, paramKey, argIndex, arg);
        return Optional.fromNullable(arg);
    }

    private boolean checkPresentOrNullable(Invocation invocation, String paramKey, int argIndex, Object arg) {
        if (arg == null
                && !invocation.getInvokable().getParameters().get(argIndex).isAnnotationPresent(Nullable.class))
            throw new NullPointerException(format("param{%s} for invocation %s.%s", paramKey,
                    invocation.getInvokable().getOwnerType().getRawType().getSimpleName(),
                    invocation.getInvokable().getName()));
        return true;
    }

    private Multimap<String, Object> getFormParamKeyValues(Invocation invocation) {
        Multimap<String, Object> formParamValues = LinkedHashMultimap.create();
        for (Parameter param : parametersWithAnnotation(invocation.getInvokable(), FormParam.class)) {
            FormParam formParam = param.getAnnotation(FormParam.class);
            String paramKey = formParam.value();
            Optional<?> paramValue = getParamValue(invocation, param.getAnnotation(ParamParser.class),
                    param.hashCode(), paramKey);
            if (paramValue.isPresent())
                formParamValues.put(paramKey, paramValue.get().toString());
        }
        return formParamValues;
    }

    private Multimap<String, Object> getQueryParamKeyValues(Invocation invocation) {
        Multimap<String, Object> queryParamValues = LinkedHashMultimap.create();
        for (Parameter param : parametersWithAnnotation(invocation.getInvokable(), QueryParam.class)) {
            QueryParam queryParam = param.getAnnotation(QueryParam.class);
            String paramKey = queryParam.value();
            Optional<?> paramValue = getParamValue(invocation, param.getAnnotation(ParamParser.class),
                    param.hashCode(), paramKey);
            if (paramValue.isPresent())
                if (paramValue.get() instanceof Iterable) {
                    @SuppressWarnings("unchecked")
                    Iterable<String> iterableStrings = transform(Iterable.class.cast(paramValue.get()),
                            toStringFunction());
                    queryParamValues.putAll(paramKey, iterableStrings);
                } else {
                    queryParamValues.put(paramKey, paramValue.get().toString());
                }
        }
        return queryParamValues;
    }

    private Map<String, Object> buildPayloadParams(Invocation invocation) {
        Map<String, Object> payloadParamValues = Maps.newLinkedHashMap();
        for (Parameter param : parametersWithAnnotation(invocation.getInvokable(), PayloadParam.class)) {
            PayloadParam payloadParam = param.getAnnotation(PayloadParam.class);
            String paramKey = payloadParam.value();
            Optional<?> paramValue = getParamValue(invocation, param.getAnnotation(ParamParser.class),
                    param.hashCode(), paramKey);
            if (paramValue.isPresent())
                payloadParamValues.put(paramKey, paramValue.get());
        }
        return payloadParamValues;
    }

    @Override
    public String toString() {
        String callerString = caller != null
                ? String.format("%s.%s%s", caller.getInvokable().getOwnerType().getRawType().getSimpleName(),
                        caller.getInvokable().getName(), caller.getArgs())
                : null;
        return Objects.toStringHelper("").omitNullValues().add("caller", callerString).toString();
    }
}