blob: 352e032ca29f84d4efd28ccfb141a5efd3d2b551 [file] [log] [blame]
/*
* 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.reflect.Reflection2.getInvokableParameters;
import static org.jclouds.util.Strings2.replaceTokens;
import static org.jclouds.util.Strings2.urlEncode;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.net.URI;
import java.util.ArrayList;
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.Encoded;
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.domain.Credentials;
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.filters.ConnectionCloseHeader;
import org.jclouds.http.filters.StripExpectHeader;
import org.jclouds.http.options.HttpRequestOptions;
import org.jclouds.http.utils.QueryValue;
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.location.Provider;
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.MoreObjects;
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;
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 Supplier<Credentials> credentials;
private final String apiVersion;
private final String buildVersion;
private final InputParamValidator inputParamValidator;
private final GetAcceptHeaders getAcceptHeaders;
private final Invocation caller;
private final boolean stripExpectHeader;
private final boolean connectionCloseHeader;
@Inject
private RestAnnotationProcessor(Injector injector,
@Provider Supplier<Credentials> credentials, @ApiVersion String apiVersion, @BuildVersion String buildVersion,
HttpUtils utils, ContentMetadataCodec contentMetadataCodec, InputParamValidator inputParamValidator,
GetAcceptHeaders getAcceptHeaders, @Nullable @Named("caller") Invocation caller,
@Named(Constants.PROPERTY_STRIP_EXPECT_HEADER) boolean stripExpectHeader,
@Named(Constants.PROPERTY_CONNECTION_CLOSE_HEADER) boolean connectionCloseHeader) {
this.injector = injector;
this.utils = utils;
this.contentMetadataCodec = contentMetadataCodec;
this.credentials = credentials;
this.apiVersion = apiVersion;
this.buildVersion = buildVersion;
this.inputParamValidator = inputParamValidator;
this.getAcceptHeaders = getAcceptHeaders;
this.caller = caller;
this.stripExpectHeader = stripExpectHeader;
this.connectionCloseHeader = connectionCloseHeader;
}
/**
* 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, getInvokableParameters(invocation.getInvokable()));
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 there is no explicit HttpRequest parameter, try to find the endpoint. When using
// delegate apis, the endpoint defined in the callee takes precedence
endpoint = getEndpointFor(invocation);
if (!endpoint.isPresent()) {
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));
if (stripExpectHeader) {
requestBuilder.filter(new StripExpectHeader());
}
if (connectionCloseHeader) {
requestBuilder.filter(new ConnectionCloseHeader());
}
Multimap<String, Object> tokenValues = LinkedHashMultimap.create();
tokenValues.put(Constants.PROPERTY_IDENTITY, credentials.get().identity);
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);
boolean encodeFullPath = !isEncodedUsed(invocation);
if (caller != null)
tokenValues.putAll(addPathAndGetTokens(caller, uriBuilder, encodeFullPath));
tokenValues.putAll(addPathAndGetTokens(invocation, uriBuilder, encodeFullPath));
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;
if (caller != null) {
headers = buildHeaders(tokenValues, caller);
headers.putAll(buildHeaders(tokenValues, invocation));
} else {
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(urlEncode(query.getKey(), '/', ','),
new QueryValue(replaceTokens(query.getValue(), tokenValues), false));
}
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.isEmpty()) {
uriBuilder.query(queryParams);
}
requestBuilder.headers(filterOutContentHeaders(headers));
// Query parameter encoding is handled in the annotation processor
requestBuilder.endpoint(uriBuilder.build(convertUnsafe(tokenValues), /*encodePath=*/encodeFullPath));
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.isEmpty()) {
if (!formParams.isEmpty()) {
parts = newLinkedList(concat(transform(formParams.entries(), ENTRY_TO_PART), parts));
}
payload = new MultipartForm(MultipartForm.BOUNDARY, parts);
} else if (!formParams.isEmpty()) {
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, tokenValues);
}
request = mapBinder.bindToRequest(request, mapParams);
} else {
request = decorateRequest(request);
}
if (request.getPayload() != null) {
contentMetadataCodec.fromHeaders(request.getPayload().getContentMetadata(), headers);
}
request = stripExpectHeaderIfContentZero(request);
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,
boolean encodeFullPath) {
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, encodeFullPath);
}
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, tokenValues).entries()) {
queryMap.put(query.getKey(), query.getValue());
}
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++) {
String key = urlEncode(query.keys()[i], '/', ',');
if (query.values()[i].equals(QueryParams.NULL)) {
queryParams.removeAll(key);
queryParams.put(key, null);
} else {
queryParams.put(key, new QueryValue(replaceTokens(query.values()[i], tokenValues), false));
}
}
}
private void addMapPayload(Map<String, Object> postParams, PayloadParams mapDefaults,
Multimap<String, String> headers, Multimap<String, Object> tokenValues) {
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(replaceTokens(mapDefaults.values()[i], headers), tokenValues));
}
}
}
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,
"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(getInvokableParameters(invokable), 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;
String host = withHost.toString();
URI baseURI = host.endsWith("/") ? withHost : URI.create(host + "/");
return baseURI.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() - getInvokableParameters(invocation.getInvokable()).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 == getInvokableParameters(invocation.getInvokable()).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 : getInvokableParameters(invokable)) {
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 static GeneratedHttpRequest stripExpectHeaderIfContentZero(GeneratedHttpRequest request) {
boolean isBodyEmpty = true;
if (request.getPayload() != null) {
Long length = request.getPayload().getContentMetadata().getContentLength();
isBodyEmpty = length != null && length == 0;
}
if (isBodyEmpty) {
request = request.toBuilder().removeHeader("Expect").build();
}
return request;
}
private boolean isEncodedUsed(Invocation invocation) {
return !parametersWithAnnotation(invocation.getInvokable(), Encoded.class).isEmpty();
}
private Multimap<String, Object> getPathParamKeyValues(Invocation invocation, boolean encodeFullPath) {
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()) {
if (!encodeFullPath && !param.isAnnotationPresent(Encoded.class)) {
pathParamValues.put(paramKey, urlEncode(paramValue.get().toString()));
} else {
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 && !getInvokableParameters(invocation.getInvokable()).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, ?> tokenValues) {
Multimap<String, Object> queryParamValues = LinkedHashMultimap.create();
for (Parameter param : parametersWithAnnotation(invocation.getInvokable(), QueryParam.class)) {
QueryParam queryParam = param.getAnnotation(QueryParam.class);
String paramKey = urlEncode(queryParam.value(), '/', ',');
Optional<?> paramValue = getParamValue(invocation, param.getAnnotation(ParamParser.class), param.hashCode(),
paramKey);
boolean encoded = param.isAnnotationPresent(Encoded.class);
if (paramValue.isPresent())
if (paramValue.get() instanceof Iterable) {
@SuppressWarnings("unchecked")
Iterable<String> iterableStrings = transform(Iterable.class.cast(paramValue.get()), toStringFunction());
List<QueryValue> values = new ArrayList<QueryValue>();
for (String stringValue : iterableStrings) {
values.add(new QueryValue(replaceTokens(stringValue, tokenValues), encoded));
}
queryParamValues.putAll(paramKey, values);
} else {
String value = paramValue.get().toString();
queryParamValues.put(paramKey, new QueryValue(replaceTokens(value, tokenValues), encoded));
}
}
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 MoreObjects.toStringHelper("").omitNullValues().add("caller", callerString).toString();
}
}