| /* |
| * 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(); |
| } |
| } |