diff --git a/core/src/main/java/org/jclouds/rest/annotations/MapPayloadParams.java b/core/src/main/java/org/jclouds/rest/annotations/MapPayloadParams.java new file mode 100644 index 0000000000..244908fbfb --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/annotations/MapPayloadParams.java @@ -0,0 +1,46 @@ +/** + * + * Copyright (C) 2010 Cloud Conscious, LLC. + * + * ==================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ + +package org.jclouds.rest.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.ws.rs.QueryParam; + +/** + * Designates that default parameters will be added a map that builds the request entity. + * + * @see QueryParam + * @author Adrian Cole + */ +@Target( { TYPE, METHOD }) +@Retention(RUNTIME) +public @interface MapPayloadParams { + + public static final String NULL = "MAP_PAYLOAD_NULL"; + + String[] keys(); + + String[] values() default NULL; +} diff --git a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java index b7180d9fe2..757a84f3b6 100755 --- a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java @@ -111,6 +111,7 @@ import org.jclouds.rest.annotations.FormParams; import org.jclouds.rest.annotations.Headers; import org.jclouds.rest.annotations.MapBinder; import org.jclouds.rest.annotations.MapPayloadParam; +import org.jclouds.rest.annotations.MapPayloadParams; import org.jclouds.rest.annotations.MatrixParams; import org.jclouds.rest.annotations.OverrideRequestFilters; import org.jclouds.rest.annotations.ParamParser; @@ -164,7 +165,7 @@ public class RestAnnotationProcessor { static final Map delegationMap = newHashMap(); static Map>> createMethodToIndexOfParamToAnnotation( - final Class annotation) { + final Class annotation) { return new MapMaker().makeComputingMap(new Function>>() { public Map> apply(final Method method) { return new MapMaker().makeComputingMap(new GetAnnotationsForMethodParameterIndex(method, annotation)); @@ -198,7 +199,7 @@ public class RestAnnotationProcessor { } private static final Class optionsVarArgsClass = new HttpRequestOptions[] {} - .getClass(); + .getClass(); private static final Function, ? extends Part> ENTRY_TO_PART = new Function, Part>() { @@ -210,17 +211,17 @@ public class RestAnnotationProcessor { }; private final Map> methodToIndexesOfOptions = new MapMaker() - .makeComputingMap(new Function>() { - public Set apply(final Method method) { - Set toReturn = newHashSet(); - for (int index = 0; index < method.getParameterTypes().length; index++) { - Class type = method.getParameterTypes()[index]; - if (HttpRequestOptions.class.isAssignableFrom(type) || optionsVarArgsClass.isAssignableFrom(type)) - toReturn.add(index); + .makeComputingMap(new Function>() { + public Set apply(final Method method) { + Set toReturn = newHashSet(); + for (int index = 0; index < method.getParameterTypes().length; index++) { + Class type = method.getParameterTypes()[index]; + if (HttpRequestOptions.class.isAssignableFrom(type) || optionsVarArgsClass.isAssignableFrom(type)) + toReturn.add(index); + } + return toReturn; } - return toReturn; - } - }); + }); private final ParseSax.Factory parserFactory; private final HttpUtils utils; @@ -238,7 +239,7 @@ public class RestAnnotationProcessor { @VisibleForTesting public static Function createResponseParser(ParseSax.Factory parserFactory, Injector injector, - Method method, HttpRequest request) { + Method method, HttpRequest request) { Function transformer; Class> handler = getSaxResponseParserClassOrNull(method); if (handler != null) { @@ -259,7 +260,7 @@ public class RestAnnotationProcessor { @VisibleForTesting public static Function createExceptionParserOrThrowResourceNotFoundOn404IfNoAnnotation( - Injector injector, Method method) { + Injector injector, Method method) { ExceptionParser annotation = method.getAnnotation(ExceptionParser.class); if (annotation != null) { return injector.getInstance(annotation.value()); @@ -270,7 +271,7 @@ public class RestAnnotationProcessor { @SuppressWarnings("unchecked") @Inject public RestAnnotationProcessor(Injector injector, ParseSax.Factory parserFactory, HttpUtils utils, - TypeLiteral typeLiteral) { + TypeLiteral typeLiteral) { this.declaring = (Class) typeLiteral.getRawType(); this.injector = injector; this.parserFactory = parserFactory; @@ -385,7 +386,7 @@ public class RestAnnotationProcessor { public GeneratedHttpRequest createRequest(Method method, Object... args) { inputParamValidator.validateMethodParametersOrThrow(method, args); ClassMethodArgs cma = logger.isTraceEnabled() ? new ClassMethodArgs(method.getDeclaringClass(), method, args) - : null; + : null; URI endpoint = callerEndpoint; try { @@ -458,7 +459,7 @@ public class RestAnnotationProcessor { } GeneratedHttpRequest request = new GeneratedHttpRequest(httpMethod, endpoint, skips, declaring, method, - args); + args); addHostHeaderIfAnnotatedWithVirtualHost(headers, request.getEndpoint().getHost(), method); addFiltersIfAnnotated(method, request); @@ -488,7 +489,7 @@ public class RestAnnotationProcessor { builder.path(clazz); builder.path(method); return builder.buildFromEncodedMap(convertUnsafe(encodeValues(getPathParamKeyValues(method, args), skips))) - .getPath(); + .getPath(); } private Multimap addPathAndGetTokens(Class clazz, Method method, Object[] args, UriBuilder builder) { @@ -503,14 +504,14 @@ public class RestAnnotationProcessor { } public static URI replaceQuery(Provider uriBuilderProvider, URI in, String newQuery, - @Nullable Comparator> sorter, char... skips) { + @Nullable Comparator> sorter, char... skips) { UriBuilder builder = uriBuilderProvider.get().uri(in); builder.replaceQuery(makeQueryLine(parseQueryToMap(newQuery), sorter, skips)); return builder.build(); } private void addMatrixParams(UriBuilder builder, Collection> tokenValues, Method method, - Object... args) { + Object... args) { if (declaring.isAnnotationPresent(MatrixParams.class)) { MatrixParams matrix = declaring.getAnnotation(MatrixParams.class); addMatrix(builder, matrix, tokenValues); @@ -527,7 +528,7 @@ public class RestAnnotationProcessor { } private Multimap addFormParams(Collection> tokenValues, Method method, - Object... args) { + Object... args) { Multimap formMap = LinkedListMultimap.create(); if (declaring.isAnnotationPresent(FormParams.class)) { FormParams form = declaring.getAnnotation(FormParams.class); @@ -546,7 +547,7 @@ public class RestAnnotationProcessor { } private Multimap addQueryParams(Collection> tokenValues, Method method, - Object... args) { + Object... args) { Multimap queryMap = LinkedListMultimap.create(); if (declaring.isAnnotationPresent(QueryParams.class)) { QueryParams query = declaring.getAnnotation(QueryParams.class); @@ -565,7 +566,7 @@ public class RestAnnotationProcessor { } private void addForm(Multimap formParams, FormParams form, - Collection> tokenValues) { + Collection> tokenValues) { for (int i = 0; i < form.keys().length; i++) { if (form.values()[i].equals(FormParams.NULL)) { formParams.removeAll(form.keys()[i]); @@ -576,8 +577,19 @@ public class RestAnnotationProcessor { } } + private void addMapPayload(Map postParams, MapPayloadParams mapDefaults, + Collection> tokenValues) { + for (int i = 0; i < mapDefaults.keys().length; i++) { + if (mapDefaults.values()[i].equals(MapPayloadParams.NULL)) { + postParams.put(mapDefaults.keys()[i], null); + } else { + postParams.put(mapDefaults.keys()[i], replaceTokens(mapDefaults.values()[i], tokenValues)); + } + } + } + private void addQuery(Multimap queryParams, QueryParams query, - Collection> tokenValues) { + Collection> tokenValues) { for (int i = 0; i < query.keys().length; i++) { if (query.values()[i].equals(QueryParams.NULL)) { queryParams.removeAll(query.keys()[i]); @@ -620,7 +632,7 @@ public class RestAnnotationProcessor { @VisibleForTesting public static URI getEndpointInParametersOrNull(Method method, final Object[] args, Injector injector) { Map> map = indexWithAtLeastOneAnnotation(method, - methodToIndexOfParamToEndpointParamAnnotations); + methodToIndexOfParamToEndpointParamAnnotations); if (map.size() >= 1 && args.length > 0) { EndpointParam firstAnnotation = (EndpointParam) get(get(map.values(), 0), 0); Function parser = injector.getInstance(firstAnnotation.parser()); @@ -630,7 +642,7 @@ public class RestAnnotationProcessor { try { URI returnVal = parser.apply(args[index]); checkArgument(returnVal != null, String.format("endpoint for [%s] not configured for %s", args[index], - method)); + method)); return returnVal; } catch (NullPointerException e) { throw new IllegalArgumentException(String.format("argument at index %d on method %s", index, method), e); @@ -648,11 +660,11 @@ public class RestAnnotationProcessor { try { URI returnVal = parser.apply(argsToParse); checkArgument(returnVal != null, String.format("endpoint for [%s] not configured for %s", argsToParse, - method)); + method)); return returnVal; } catch (NullPointerException e) { throw new IllegalArgumentException(String.format("argument at indexes %s on method %s", map.keySet(), - method), e); + method), e); } } } @@ -693,13 +705,13 @@ public class RestAnnotationProcessor { ResponseParser annotation = method.getAnnotation(ResponseParser.class); if (annotation == null) { if (method.getReturnType().equals(void.class) - || TypeLiteral.get(method.getGenericReturnType()).equals(futureVoidLiteral)) { + || TypeLiteral.get(method.getGenericReturnType()).equals(futureVoidLiteral)) { return Key.get(ReleasePayloadAndReturn.class); } else if (method.getReturnType().equals(boolean.class) || method.getReturnType().equals(Boolean.class) - || TypeLiteral.get(method.getGenericReturnType()).equals(futureBooleanLiteral)) { + || TypeLiteral.get(method.getGenericReturnType()).equals(futureBooleanLiteral)) { return Key.get(ReturnTrueIf2xx.class); } else if (method.getReturnType().equals(InputStream.class) - || TypeLiteral.get(method.getGenericReturnType()).equals(futureInputStreamLiteral)) { + || TypeLiteral.get(method.getGenericReturnType()).equals(futureInputStreamLiteral)) { return Key.get(ReturnInputStream.class); } else if (getAcceptHeadersOrNull(method).contains(MediaType.APPLICATION_JSON)) { Type returnVal; @@ -720,10 +732,10 @@ public class RestAnnotationProcessor { parserType = Types.newParameterizedType(ParseJson.class, returnVal); return (Key>) Key.get(parserType); } else if (method.getReturnType().equals(String.class) - || TypeLiteral.get(method.getGenericReturnType()).equals(futureStringLiteral)) { + || TypeLiteral.get(method.getGenericReturnType()).equals(futureStringLiteral)) { return Key.get(ReturnStringIf2xx.class); } else if (method.getReturnType().equals(URI.class) - || TypeLiteral.get(method.getGenericReturnType()).equals(futureURILiteral)) { + || TypeLiteral.get(method.getGenericReturnType()).equals(futureURILiteral)) { return Key.get(ParseURIFromListOrLocationHeaderIf20x.class); } else { throw new IllegalStateException("You must specify a ResponseParser annotation on: " + method.toString()); @@ -755,7 +767,7 @@ public class RestAnnotationProcessor { } else { if (postBinders[0] instanceof org.jclouds.rest.MapBinder) { throw new IllegalArgumentException("we currently do not support multiple varargs postBinders in: " - + method.getName()); + + method.getName()); } } } else if (arg instanceof org.jclouds.rest.MapBinder) { @@ -792,8 +804,8 @@ public class RestAnnotationProcessor { Set requests = IsHttpMethod.getHttpMethods(method); if (requests == null || requests.size() != 1) { throw new IllegalStateException( - "You must use at least one, but no more than one http method or pathparam annotation on: " - + method.toString()); + "You must use at least one, but no more than one http method or pathparam annotation on: " + + method.toString()); } return requests.iterator().next(); } @@ -806,22 +818,21 @@ public class RestAnnotationProcessor { public void decorateRequest(GeneratedHttpRequest request, Multimap headers) { org.jclouds.rest.MapBinder mapBinder = getMapPayloadBinderOrNull(request.getJavaMethod(), request.getArgs()); - Map mapParams = buildPostParams(request.getJavaMethod(), request.getArgs()); - // MapPayloadBinder is only useful if there are parameters. We guard here - // in case the - // MapPayloadBinder is also an PayloadBinder. If so, it can be used with - // or without - // parameters. if (mapBinder != null) { + Map mapParams = buildPostParams(request.getJavaMethod(), request.getArgs()); + if (request.getJavaMethod().isAnnotationPresent(MapPayloadParams.class)) { + MapPayloadParams params = request.getJavaMethod().getAnnotation(MapPayloadParams.class); + addMapPayload(mapParams, params, headers.entries()); + } mapBinder.bindToRequest(request, mapParams); } else { OUTER: for (Entry> entry : filterValues( - methodToIndexOfParamToDecoratorParamAnnotation.get(request.getJavaMethod()), - new Predicate>() { - public boolean apply(Set input) { - return input.size() >= 1; - } - }).entrySet()) { + methodToIndexOfParamToDecoratorParamAnnotation.get(request.getJavaMethod()), + new Predicate>() { + public boolean apply(Set input) { + return input.size() >= 1; + } + }).entrySet()) { boolean shouldBreak = false; BinderParam payloadAnnotation = (BinderParam) entry.getValue().iterator().next(); Binder binder = injector.getInstance(payloadAnnotation.value()); @@ -857,24 +868,24 @@ public class RestAnnotationProcessor { } public static Map> indexWithOnlyOneAnnotation(Method method, String description, - Map>> toRefine) { + Map>> toRefine) { Map> indexToPayloadAnnotation = indexWithAtLeastOneAnnotation(method, toRefine); if (indexToPayloadAnnotation.size() > 1) { throw new IllegalStateException(String.format( - "You must not specify more than one %s annotation on: %s; found %s", description, method.toString(), - indexToPayloadAnnotation)); + "You must not specify more than one %s annotation on: %s; found %s", description, method.toString(), + indexToPayloadAnnotation)); } return indexToPayloadAnnotation; } private static Map> indexWithAtLeastOneAnnotation(Method method, - Map>> toRefine) { + Map>> toRefine) { Map> indexToPayloadAnnotation = filterValues(toRefine.get(method), - new Predicate>() { - public boolean apply(Set input) { - return input.size() == 1; - } - }); + new Predicate>() { + public boolean apply(Set input) { + return input.size() == 1; + } + }); return indexToPayloadAnnotation; } @@ -893,7 +904,7 @@ public class RestAnnotationProcessor { } else { if (options[0] instanceof HttpRequestOptions) { throw new IllegalArgumentException("we currently do not support multiple varargs options in: " - + method.getName()); + + method.getName()); } } } else { @@ -905,7 +916,7 @@ public class RestAnnotationProcessor { } public Multimap buildHeaders(Collection> tokenValues, Method method, - final Object... args) { + final Object... args) { Multimap headers = LinkedHashMultimap.create(); addHeaderIfAnnotationPresentOnMethod(headers, method, tokenValues); Map> indexToHeaderParam = methodToIndexOfParamToHeaderParamAnnotations.get(method); @@ -952,7 +963,7 @@ public class RestAnnotationProcessor { } public void addHeaderIfAnnotationPresentOnMethod(Multimap headers, Method method, - Collection> tokenValues) { + Collection> tokenValues) { if (declaring.isAnnotationPresent(Headers.class)) { Headers header = declaring.getAnnotation(Headers.class); addHeader(headers, header, tokenValues); @@ -964,7 +975,7 @@ public class RestAnnotationProcessor { } private void addHeader(Multimap headers, Headers header, - Collection> tokenValues) { + Collection> tokenValues) { for (int i = 0; i < header.keys().length; i++) { String value = header.values()[i]; value = replaceTokens(value, tokenValues); diff --git a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java index aec859bd2c..0aefbd39ff 100755 --- a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java @@ -119,6 +119,7 @@ import org.jclouds.rest.annotations.FormParams; import org.jclouds.rest.annotations.Headers; import org.jclouds.rest.annotations.MapBinder; import org.jclouds.rest.annotations.MapPayloadParam; +import org.jclouds.rest.annotations.MapPayloadParams; import org.jclouds.rest.annotations.MatrixParams; import org.jclouds.rest.annotations.OverrideRequestFilters; import org.jclouds.rest.annotations.ParamParser; @@ -534,25 +535,27 @@ public class RestAnnotationProcessorTest extends BaseRestClientTest { assertEquals(request.getMethod(), "POST"); } - public class TestPost { + public interface TestPost { @POST - public void post(@Nullable @BinderParam(BindToStringPayload.class) String content) { - } + void post(@Nullable @BinderParam(BindToStringPayload.class) String content); @POST - public void postAsJson(@BinderParam(BindToJsonPayload.class) String content) { - } + public void postAsJson(@BinderParam(BindToJsonPayload.class) String content); @POST @Path("{foo}") - public void postWithPath(@PathParam("foo") @MapPayloadParam("fooble") String path, MapBinder content) { - } + public void postWithPath(@PathParam("foo") @MapPayloadParam("fooble") String path, MapBinder content); @POST @Path("{foo}") @MapBinder(BindToJsonPayload.class) - public void postWithMethodBinder(@PathParam("foo") @MapPayloadParam("fooble") String path) { - } + public void postWithMethodBinder(@PathParam("foo") @MapPayloadParam("fooble") String path); + + @POST + @Path("{foo}") + @MapPayloadParams(keys = "rat", values = "atat") + @MapBinder(BindToJsonPayload.class) + public void postWithMethodBinderAndDefaults(@PathParam("foo") @MapPayloadParam("fooble") String path); } public void testCreatePostRequest() throws SecurityException, NoSuchMethodException, IOException { @@ -606,6 +609,15 @@ public class RestAnnotationProcessorTest extends BaseRestClientTest { assertPayloadEquals(request, "{\"fooble\":\"data\"}", "application/json", false); } + public void testCreatePostWithMethodBinderAndDefaults() throws SecurityException, NoSuchMethodException, IOException { + Method method = TestPost.class.getMethod("postWithMethodBinderAndDefaults", String.class); + HttpRequest request = factory(TestPost.class).createRequest(method, "data"); + + assertRequestLineEquals(request, "POST http://localhost:9999/data HTTP/1.1"); + assertNonPayloadHeadersEqual(request, ""); + assertPayloadEquals(request, "{\"fooble\":\"data\",\"rat\":\"atat\"}", "application/json", false); + } + static interface TestMultipartForm { @POST void withStringPart(@PartParam(name = "fooble") String path);