From 6de55b5fd63cea42f95d91ad9bf2d9d63c717d74 Mon Sep 17 00:00:00 2001 From: "adrian.f.cole" Date: Wed, 23 Sep 2009 23:15:43 +0000 Subject: [PATCH] Issue 76: added ability to specify multiple default headers and queries, as well addedd a common collection type git-svn-id: http://jclouds.googlecode.com/svn/trunk@1901 3d8758e0-26b5-11de-8745-db77d3ebf521 --- .../org/jclouds/rest/ArrayBoundedList.java | 56 +++++++ .../java/org/jclouds/rest/BoundedList.java | 41 +++++ .../main/java/org/jclouds/rest/Headers.java | 63 +++++++ .../rest/JaxrsAnnotationProcessor.java | 158 +++++++++++++----- .../java/org/jclouds/rest/QueryParams.java | 49 ++++++ .../org/jclouds/rest/RestClientProxy.java | 3 +- .../rest/JaxrsAnnotationProcessorTest.java | 90 +++++++++- 7 files changed, 407 insertions(+), 53 deletions(-) create mode 100644 core/src/main/java/org/jclouds/rest/ArrayBoundedList.java create mode 100644 core/src/main/java/org/jclouds/rest/BoundedList.java create mode 100755 core/src/main/java/org/jclouds/rest/Headers.java create mode 100755 core/src/main/java/org/jclouds/rest/QueryParams.java diff --git a/core/src/main/java/org/jclouds/rest/ArrayBoundedList.java b/core/src/main/java/org/jclouds/rest/ArrayBoundedList.java new file mode 100644 index 0000000000..a31323e505 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/ArrayBoundedList.java @@ -0,0 +1,56 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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; + +import java.util.ArrayList; +import java.util.List; + +public class ArrayBoundedList extends ArrayList implements BoundedList { + + /** The serialVersionUID */ + private static final long serialVersionUID = -7133632087734650835L; + protected final String prefix; + protected final String marker; + protected final int maxResults; + + public ArrayBoundedList(List contents, String prefix, String marker, int maxResults) { + this.addAll(contents); + this.prefix = prefix; + this.marker = marker; + this.maxResults = maxResults; + } + + public String getPrefix() { + return prefix; + } + + public String getMarker() { + return marker; + } + + public int getMaxResults() { + return maxResults; + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/BoundedList.java b/core/src/main/java/org/jclouds/rest/BoundedList.java new file mode 100644 index 0000000000..99422008cb --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/BoundedList.java @@ -0,0 +1,41 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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; + +import java.util.List; + +/** + * + * @author Adrian Cole + * + */ +public interface BoundedList extends List { + + String getPrefix(); + + String getMarker(); + + int getMaxResults(); + +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/Headers.java b/core/src/main/java/org/jclouds/rest/Headers.java new file mode 100755 index 0000000000..d2429cf192 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/Headers.java @@ -0,0 +1,63 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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; + +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.PathParam; +import javax.ws.rs.core.HttpHeaders; + +/** + * Designates that a header will be added to the request. This header will contain the specified + * {@code value}, expanding any variables annotated with {@code PathParam}. + * + * @see PathParam + * @author Adrian Cole + */ +@Target( { TYPE, METHOD }) +@Retention(RUNTIME) +public @interface Headers { + + /** + * @see HttpHeaders + */ + String [] keys(); + +/** + * can be defined literally, or with enclosed variables (ex. {variable}) + *

+ * The inputs to these variables are taken from method parameters annotated with {@code + * @PathParam}. + * + * @see PathParam + * + * @return + */ + String [] values(); +} diff --git a/core/src/main/java/org/jclouds/rest/JaxrsAnnotationProcessor.java b/core/src/main/java/org/jclouds/rest/JaxrsAnnotationProcessor.java index 7cf2c88afb..1a9139afdf 100644 --- a/core/src/main/java/org/jclouds/rest/JaxrsAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/JaxrsAnnotationProcessor.java @@ -23,6 +23,9 @@ */ package org.jclouds.rest; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + import java.io.UnsupportedEncodingException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -90,6 +93,7 @@ public class JaxrsAnnotationProcessor { private final Map>> methodToindexOfParamToPathParamAnnotations = createMethodToIndexOfParamToAnnotation(PathParam.class); private final Map>> methodToindexOfParamToPostParamAnnotations = createMethodToIndexOfParamToAnnotation(MapEntityParam.class); private final Map>> methodToindexOfParamToParamParserAnnotations = createMethodToIndexOfParamToAnnotation(ParamParser.class); + private final Map delegationMap = Maps.newHashMap(); static Map>> createMethodToIndexOfParamToAnnotation( final Class annotation) { @@ -182,9 +186,14 @@ public class JaxrsAnnotationProcessor { seedCache(declaring); } + protected Method getDelegateOrNull(Method in) { + return delegationMap.get(new MethodKey(in)); + } + private void seedCache(Class declaring) { Set methods = Sets.newHashSet(declaring.getMethods()); - for (Method method : Sets.difference(methods, Sets.newHashSet(Object.class.getMethods()))) { + methods = Sets.difference(methods, Sets.newHashSet(Object.class.getMethods())); + for (Method method : methods) { if (isHttpMethod(method)) { for (int index = 0; index < method.getParameterTypes().length; index++) { methodToIndexOfParamToEntityAnnotation.get(method).get(index); @@ -195,14 +204,58 @@ public class JaxrsAnnotationProcessor { methodToindexOfParamToParamParserAnnotations.get(method).get(index); methodToIndexesOfOptions.get(method); } + delegationMap.put(new MethodKey(method), method); } else if (isConstantDeclaration(method)) { bindConstant(method); + } else if (!method.getDeclaringClass().equals(declaring)) { + logger.debug("skipping potentially overridden method", method); } else { - throw new RuntimeException("Method is not annotated as either http or constant"); + throw new RuntimeException("Method is not annotated as either http or constant: " + + method); } } } + public static class MethodKey { + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + parameterCount; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MethodKey other = (MethodKey) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (parameterCount != other.parameterCount) + return false; + return true; + } + + private final String name; + private final int parameterCount; + + public MethodKey(Method method) { + this.name = method.getName(); + this.parameterCount = method.getParameterTypes().length; + } + + } + final Injector injector; private HttpRequestOptionsBinder optionsBinder; @@ -214,13 +267,13 @@ public class JaxrsAnnotationProcessor { builder.path(declaring); builder.path(method); - if (declaring.isAnnotationPresent(Query.class)) { - Query query = declaring.getAnnotation(Query.class); + if (declaring.isAnnotationPresent(QueryParams.class)) { + QueryParams query = declaring.getAnnotation(QueryParams.class); addQuery(builder, query); } - if (method.isAnnotationPresent(Query.class)) { - Query query = method.getAnnotation(Query.class); + if (method.isAnnotationPresent(QueryParams.class)) { + QueryParams query = method.getAnnotation(QueryParams.class); addQuery(builder, query); } @@ -228,6 +281,7 @@ public class JaxrsAnnotationProcessor { HttpRequestOptions options = findOptionsIn(method, args); if (options != null) { + injector.injectMembers(options);// TODO test case headers.putAll(options.buildRequestHeaders()); for (Entry query : options.buildQueryParameters().entries()) { builder.queryParam(query.getKey(), query.getValue()); @@ -261,24 +315,33 @@ public class JaxrsAnnotationProcessor { return request; } - private void addQuery(UriBuilder builder, Query query) { - if (query.value().equals(Query.NULL)) - builder.replaceQuery(query.key()); - else - builder.queryParam(query.key(), query.value()); + private void addQuery(UriBuilder builder, QueryParams query) { + for (int i = 0; i < query.keys().length; i++) { + if (query.values()[i].equals(QueryParams.NULL)) { + builder.replaceQuery(query.keys()[i]); + } else { + builder.queryParam(query.keys()[i], query.values()[i]); + } + } } private void addFiltersIfAnnotated(Method method, HttpRequest request) { if (declaring.isAnnotationPresent(RequestFilters.class)) { for (Class clazz : declaring.getAnnotation( RequestFilters.class).value()) { - request.getFilters().add(injector.getInstance(clazz)); + HttpRequestFilter instance = injector.getInstance(clazz); + request.getFilters().add(instance); + logger.debug("%s - adding filter %s from annotation on %s", request, instance, + declaring.getName()); } } if (method.isAnnotationPresent(RequestFilters.class)) { for (Class clazz : method.getAnnotation(RequestFilters.class) .value()) { - request.getFilters().add(injector.getInstance(clazz)); + HttpRequestFilter instance = injector.getInstance(clazz); + request.getFilters().add(instance); + logger.debug("%s - adding filter %s from annotation on %s", request, instance, method + .getName()); } } } @@ -290,7 +353,10 @@ public class JaxrsAnnotationProcessor { HostPrefixParam param = (HostPrefixParam) map.values().iterator().next().iterator().next(); int index = map.keySet().iterator().next(); - String prefix = args[index].toString(); + String prefix = checkNotNull(args[index], + String.format("argument at index %d on method %s", index, method)).toString(); + checkArgument(!prefix.equals(""), String.format( + "argument at index %d must be a valid hostname for method %s", index, method)); String joinOn = param.value(); String host = endpoint.getHost(); @@ -332,27 +398,29 @@ public class JaxrsAnnotationProcessor { } public MapEntityBinder getMapEntityBinderOrNull(Method method, Object[] args) { - for (Object arg : args) { - if (arg instanceof Object[]) { - Object[] postBinders = (Object[]) arg; - if (postBinders.length == 0) { - } else if (postBinders.length == 1) { - if (postBinders[0] instanceof MapEntityBinder) { - MapEntityBinder binder = (MapEntityBinder) postBinders[0]; - injector.injectMembers(binder); - return binder; - } - } else { - if (postBinders[0] instanceof MapEntityBinder) { - throw new IllegalArgumentException( - "we currently do not support multiple varargs postBinders in: " - + method.getName()); + if (args != null) { + for (Object arg : args) { + if (arg instanceof Object[]) { + Object[] postBinders = (Object[]) arg; + if (postBinders.length == 0) { + } else if (postBinders.length == 1) { + if (postBinders[0] instanceof MapEntityBinder) { + MapEntityBinder binder = (MapEntityBinder) postBinders[0]; + injector.injectMembers(binder); + return binder; + } + } else { + if (postBinders[0] instanceof MapEntityBinder) { + throw new IllegalArgumentException( + "we currently do not support multiple varargs postBinders in: " + + method.getName()); + } } + } else if (arg instanceof MapEntityBinder) { + MapEntityBinder binder = (MapEntityBinder) arg; + injector.injectMembers(binder); + return binder; } - } else if (arg instanceof MapEntityBinder) { - MapEntityBinder binder = (MapEntityBinder) arg; - injector.injectMembers(binder); - return binder; } } MapBinder annotation = method.getAnnotation(MapBinder.class); @@ -515,24 +583,28 @@ public class JaxrsAnnotationProcessor { public void addHeaderIfAnnotationPresentOnMethod(Multimap headers, Method method, Object[] args, char... skipEncode) throws UnsupportedEncodingException { - if (declaring.isAnnotationPresent(Header.class)) { - Header header = declaring.getAnnotation(Header.class); + if (declaring.isAnnotationPresent(Headers.class)) { + Headers header = declaring.getAnnotation(Headers.class); addHeader(headers, method, args, header); } - if (method.isAnnotationPresent(Header.class)) { - Header header = method.getAnnotation(Header.class); + if (method.isAnnotationPresent(Headers.class)) { + Headers header = method.getAnnotation(Headers.class); addHeader(headers, method, args, header); } } private void addHeader(Multimap headers, Method method, Object[] args, - Header header) throws UnsupportedEncodingException { - String value = header.value(); - for (Entry tokenValue : getEncodedPathParamKeyValues(method, args).entrySet()) { - value = value.replaceAll("\\{" + tokenValue.getKey() + "\\}", tokenValue.getValue() - .toString()); + Headers header) throws UnsupportedEncodingException { + for (int i = 0; i < header.keys().length; i++) { + String value = header.values()[i]; + for (Entry tokenValue : getEncodedPathParamKeyValues(method, args) + .entrySet()) { + value = value.replaceAll("\\{" + tokenValue.getKey() + "\\}", tokenValue.getValue() + .toString()); + } + headers.put(header.keys()[i], value); } - headers.put(header.key(), value); + } private Map getEncodedPathParamKeyValues(Method method, Object[] args, diff --git a/core/src/main/java/org/jclouds/rest/QueryParams.java b/core/src/main/java/org/jclouds/rest/QueryParams.java new file mode 100755 index 0000000000..92731b1936 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/QueryParams.java @@ -0,0 +1,49 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.ws.rs.QueryParam; + +/** + * Designates that a query will be added to the request. + * + * @see QueryParam + * @author Adrian Cole + */ +@Target( { TYPE, METHOD }) +@Retention(RUNTIME) +public @interface QueryParams { + + public static final String NULL = "QUERY_NULL"; + + String [] keys(); + + String [] values() default NULL; +} diff --git a/core/src/main/java/org/jclouds/rest/RestClientProxy.java b/core/src/main/java/org/jclouds/rest/RestClientProxy.java index d38e370d54..88228372b7 100644 --- a/core/src/main/java/org/jclouds/rest/RestClientProxy.java +++ b/core/src/main/java/org/jclouds/rest/RestClientProxy.java @@ -84,7 +84,8 @@ public class RestClientProxy implements InvocationHandler { return this.equals(o); } else if (method.getName().equals("hashCode")) { return this.hashCode(); - } else if (util.isHttpMethod(method)) { + } else if (util.getDelegateOrNull(method) != null) { + method = util.getDelegateOrNull(method); logger.trace("%s - converting method to request", method); HttpRequest request = util.createRequest(endPoint, method, args); logger.trace("%s - converted method to request %s", method, request); diff --git a/core/src/test/java/org/jclouds/rest/JaxrsAnnotationProcessorTest.java b/core/src/test/java/org/jclouds/rest/JaxrsAnnotationProcessorTest.java index b44b16debd..b0f485ae7e 100644 --- a/core/src/test/java/org/jclouds/rest/JaxrsAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/JaxrsAnnotationProcessorTest.java @@ -90,12 +90,17 @@ public class JaxrsAnnotationProcessorTest { public @interface FOO { } - @Query(key = "x-ms-version", value = "2009-07-17") + @QueryParams(keys = "x-ms-version", values = "2009-07-17") public class TestQuery { @FOO - @Query(key = "x-ms-rubbish", value = "bin") + @QueryParams(keys = "x-ms-rubbish", values = "bin") public void foo() { } + + @FOO + @QueryParams(keys = { "foo", "fooble" }, values = { "bar", "baz" }) + public void foo2() { + } } public void testQuery() throws SecurityException, NoSuchMethodException { @@ -109,6 +114,18 @@ public class JaxrsAnnotationProcessorTest { assertEquals(httpMethod.getMethod(), "FOO"); } + public void testQuery2() throws SecurityException, NoSuchMethodException { + Method method = TestQuery.class.getMethod("foo2"); + URI endpoint = URI.create("http://localhost"); + HttpRequest httpMethod = factory.create(TestQuery.class).createRequest(endpoint, method, + new Object[] {}); + assertEquals(httpMethod.getEndpoint().getHost(), "localhost"); + assertEquals(httpMethod.getEndpoint().getPath(), ""); + assertEquals(httpMethod.getEndpoint().getQuery(), + "x-ms-version=2009-07-17&foo=bar&fooble=baz"); + assertEquals(httpMethod.getMethod(), "FOO"); + } + public class TestCustomMethod { @FOO public void foo() { @@ -125,6 +142,27 @@ public class JaxrsAnnotationProcessorTest { assertEquals(httpMethod.getMethod(), "FOO"); } + public interface Parent { + public void foo(); + } + + public class TestOverridden implements Parent { + @POST + public void foo() { + + } + } + + public void testOverriddenMethod() throws SecurityException, NoSuchMethodException { + Method method = TestOverridden.class.getMethod("foo"); + URI endpoint = URI.create("http://localhost"); + HttpRequest httpMethod = factory.create(TestOverridden.class).createRequest(endpoint, method, + new Object[] {}); + assertEquals(httpMethod.getEndpoint().getHost(), "localhost"); + assertEquals(httpMethod.getEndpoint().getPath(), ""); + assertEquals(httpMethod.getMethod(), "POST"); + } + public class TestPost { @POST public void post(@EntityParam String content) { @@ -376,23 +414,40 @@ public class JaxrsAnnotationProcessorTest { public class TestHeader { @GET - @Header(key = "x-amz-copy-source", value = "/{bucket}") + @Headers(keys = "x-amz-copy-source", values = "/{bucket}") public void oneHeader(@PathParam("bucket") String path) { } @GET - @Header(key = "x-amz-copy-source", value = "/{bucket}/{key}") + @Headers(keys = { "slash", "hyphen" }, values = { "/{bucket}", "-{bucket}" }) + public void twoHeader(@PathParam("bucket") String path) { + } + + @GET + @Headers(keys = "x-amz-copy-source", values = "/{bucket}/{key}") public void twoHeaders(@PathParam("bucket") String path, @PathParam("key") String path2) { } @GET - @Header(key = "x-amz-copy-source", value = "/{bucket}/{key}") + @Headers(keys = "x-amz-copy-source", values = "/{bucket}/{key}") public void twoHeadersOutOfOrder(@PathParam("key") String path, @PathParam("bucket") String path2) { } } - @Header(key = "x-amz-copy-source", value = "/{bucket}") + @Test + public void testBuildTwoHeader() throws SecurityException, NoSuchMethodException, + UnsupportedEncodingException { + Method oneHeader = TestHeader.class.getMethod("twoHeader", String.class); + Multimap headers = HashMultimap.create(); + factory.create(TestHeader.class).addHeaderIfAnnotationPresentOnMethod(headers, oneHeader, + new Object[] { "robot" }); + assertEquals(headers.size(), 2); + assertEquals(headers.get("slash"), Collections.singletonList("/robot")); + assertEquals(headers.get("hyphen"), Collections.singletonList("-robot")); + } + + @Headers(keys = "x-amz-copy-source", values = "/{bucket}") public class TestClassHeader { @GET @@ -501,14 +556,14 @@ public class JaxrsAnnotationProcessorTest { @GET @Path("/{id}") - @Query(key = "max-keys", value = "0") + @QueryParams(keys = "max-keys", values = "0") public Future getQuery(@PathParam("id") String id) { return null; } @GET @Path("/{id}") - @Query(key = "acl") + @QueryParams(keys = "acl") public Future getQueryNull(@PathParam("id") String id) { return null; } @@ -529,7 +584,7 @@ public class JaxrsAnnotationProcessorTest { @PUT @Path("/{id}") - @Header(key = "foo", value = "--{id}--") + @Headers(keys = "foo", values = "--{id}--") @ResponseParser(ReturnTrueIf2xx.class) public Future putHeader(@PathParam("id") String id, @EntityParam String payload) { return null; @@ -798,6 +853,23 @@ public class JaxrsAnnotationProcessorTest { assertEquals(httpMethod.getHeaders().size(), 0); } + @Test(expectedExceptions = IllegalArgumentException.class) + public void testHostPrefixDotEmpty() throws SecurityException, NoSuchMethodException { + Method method = TestVirtualHost.class.getMethod("getPrefixDot", String.class, String.class); + URI endpoint = URI.create("http://localhost"); + factory.create(TestVirtualHost.class).createRequest(endpoint, method, + new Object[] { "1", "" }); + + } + + @Test(expectedExceptions = NullPointerException.class) + public void testHostPrefixDotNull() throws SecurityException, NoSuchMethodException { + Method method = TestVirtualHost.class.getMethod("getPrefixDot", String.class, String.class); + URI endpoint = URI.create("http://localhost"); + factory.create(TestVirtualHost.class).createRequest(endpoint, method, + new Object[] { "1", null }); + } + public class TestHeaders { @GET