From f1d81eaf12f8d37ae2180d459f7bb3227ef2a783 Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Thu, 3 Jan 2013 13:56:55 +0100 Subject: [PATCH] Make JAXBResponseParser parameterizable By default, RestAnnotationProcessor builds the JAXBParser with the return type of the invoked method. This, however, can make it impossible to combine the JAXBResponseParser with the Transform annotation. The payload of the response to be parsed with the JAXBResponseParser could be of a different type than the output generated by the transformer function. This makes it necessary to configure the type for the response parser in the JAXBResponseParser annotation and set the return type of the invoked method to the type generated by the transformer function. --- .../rest/annotations/JAXBResponseParser.java | 10 ++ .../internal/RestAnnotationProcessor.java | 15 +- ...AXBResponseParserAnnotationExpectTest.java | 158 ++++++++++++++++++ .../rest/binders/BindToXMLPayloadTest.java | 26 ++- .../internal/RestAnnotationProcessorTest.java | 81 --------- 5 files changed, 200 insertions(+), 90 deletions(-) create mode 100644 core/src/test/java/org/jclouds/rest/annotationparsing/JAXBResponseParserAnnotationExpectTest.java diff --git a/core/src/main/java/org/jclouds/rest/annotations/JAXBResponseParser.java b/core/src/main/java/org/jclouds/rest/annotations/JAXBResponseParser.java index c6cb594ff4..df2d07b8fe 100644 --- a/core/src/main/java/org/jclouds/rest/annotations/JAXBResponseParser.java +++ b/core/src/main/java/org/jclouds/rest/annotations/JAXBResponseParser.java @@ -24,6 +24,10 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import javax.lang.model.type.NullType; + +import org.jclouds.http.functions.ParseXMLWithJAXB; + /** * Shows the transformer type used to parse XML with the * {@link ParseXMLWithJAXB} parser in a HttpResponse. @@ -34,4 +38,10 @@ import java.lang.annotation.Target; @Retention(RUNTIME) public @interface JAXBResponseParser { + /** + * If present, this is the class that will be used to unmarshal the XML + * document. If omitted, the return type of the annotated method will be + * used. + */ + Class value() default NullType.class; } 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 8e8a514a66..3d8e8570f3 100644 --- a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java @@ -67,6 +67,7 @@ import java.util.Set; import java.util.SortedSet; import javax.annotation.Resource; +import javax.lang.model.type.NullType; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.HeaderParam; @@ -815,10 +816,16 @@ public abstract class RestAnnotationProcessor { @SuppressWarnings("unchecked") public static Key> getJAXBParserKeyForMethod(Method method) { - Type returnVal = getReturnTypeForMethod(method); - Type parserType = Types.newParameterizedType(ParseXMLWithJAXB.class, returnVal); - return (Key>) Key.get(parserType); - } + Optional configuredReturnVal = Optional.absent(); + if (method.isAnnotationPresent(JAXBResponseParser.class)) { + Type configuredClass = method.getAnnotation(JAXBResponseParser.class).value(); + configuredReturnVal = configuredClass.equals(NullType.class) ? Optional. absent() : Optional + . of(configuredClass); + } + Type returnVal = configuredReturnVal.or(getReturnTypeForMethod(method)); + Type parserType = Types.newParameterizedType(ParseXMLWithJAXB.class, returnVal); + return (Key>) Key.get(parserType); + } public static Type getReturnTypeForMethod(Method method) { Type returnVal; diff --git a/core/src/test/java/org/jclouds/rest/annotationparsing/JAXBResponseParserAnnotationExpectTest.java b/core/src/test/java/org/jclouds/rest/annotationparsing/JAXBResponseParserAnnotationExpectTest.java new file mode 100644 index 0000000000..f4a918cf12 --- /dev/null +++ b/core/src/test/java/org/jclouds/rest/annotationparsing/JAXBResponseParserAnnotationExpectTest.java @@ -0,0 +1,158 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.annotationparsing; + +import static org.jclouds.providers.AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.xml.bind.annotation.XmlRootElement; + +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.providers.ProviderMetadata; +import org.jclouds.rest.annotations.JAXBResponseParser; +import org.jclouds.rest.annotations.Transform; +import org.jclouds.rest.internal.BaseRestClientExpectTest; +import org.testng.annotations.Test; + +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Tests the use of the {@link JAXBResponseParser} annotation. + * + * @author Ignasi Barrera + */ +@Test(groups = "unit", testName = "JAXBResponseParserAnnotationExpectTest") +public class JAXBResponseParserAnnotationExpectTest extends + BaseRestClientExpectTest { + + @XmlRootElement(name = "test") + public static class TestJAXBDomain { + private String elem; + + public String getElem() { + return elem; + } + + public void setElem(String elem) { + this.elem = elem; + } + + @Override + public String toString() { + return "TestJAXBDomain [elem=" + elem + "]"; + } + + } + + public interface TestJAXBApi { + public TestJAXBDomain jaxbGetWithAnnotation(); + + public Object jaxbGetWithAnnotationAndCustomClass(); + + public TestJAXBDomain jaxbGetWithAcceptHeader(); + + public String jaxbGetWithTransformer(); + } + + public interface TestJAXBAsyncApi { + @GET + @Path("/jaxb/annotation") + @JAXBResponseParser + public ListenableFuture jaxbGetWithAnnotation(); + + @GET + @Path("/jaxb/custom") + @JAXBResponseParser(TestJAXBDomain.class) + public ListenableFuture jaxbGetWithAnnotationAndCustomClass(); + + @GET + @Path("/jaxb/header") + @Consumes(MediaType.APPLICATION_XML) + public ListenableFuture jaxbGetWithAcceptHeader(); + + @GET + @Path("/jaxb/transformer") + @JAXBResponseParser(TestJAXBDomain.class) + @Transform(ToString.class) + public ListenableFuture jaxbGetWithTransformer(); + } + + private static class ToString implements Function { + @Override + public String apply(Object input) { + return Functions.toStringFunction().apply(input); + } + } + + @Test + public void testJAXBResponseParserAnnotationWithoutValue() throws SecurityException, NoSuchMethodException { + TestJAXBApi api = requestSendsResponse( // + HttpRequest.builder().method("GET").endpoint("http://mock/jaxb/annotation").build(), // + HttpResponse.builder().statusCode(200).payload("Hello World").build()); + + TestJAXBDomain result = api.jaxbGetWithAnnotation(); + assertEquals(result.getElem(), "Hello World"); + } + + @Test + public void testJAXBResponseParserAnnotationWithCustomValue() throws SecurityException, NoSuchMethodException { + TestJAXBApi api = requestSendsResponse( // + HttpRequest.builder().method("GET").endpoint("http://mock/jaxb/custom").build(), // + HttpResponse.builder().statusCode(200).payload("Hello World").build()); + + Object result = api.jaxbGetWithAnnotationAndCustomClass(); + assertTrue(result instanceof TestJAXBDomain); + assertEquals(TestJAXBDomain.class.cast(result).getElem(), "Hello World"); + } + + @Test + public void testJAXBResponseParserAnnotationWithAcceptHeader() throws SecurityException, NoSuchMethodException { + TestJAXBApi api = requestSendsResponse( // + HttpRequest.builder().method("GET").endpoint("http://mock/jaxb/header") + .addHeader("Accept", MediaType.APPLICATION_XML).build(), // + HttpResponse.builder().statusCode(200).payload("Hello World").build()); + + TestJAXBDomain result = api.jaxbGetWithAcceptHeader(); + assertEquals(result.getElem(), "Hello World"); + } + + @Test + public void testJAXBResponseParserAnnotationWithTransformer() throws SecurityException, NoSuchMethodException { + TestJAXBApi api = requestSendsResponse( // + HttpRequest.builder().method("GET").endpoint("http://mock/jaxb/transformer").build(), // + HttpResponse.builder().statusCode(200).payload("Hello World").build()); + + String result = api.jaxbGetWithTransformer(); + assertEquals(result, "TestJAXBDomain [elem=Hello World]"); + } + + @Override + public ProviderMetadata createProviderMetadata() { + return forClientMappedToAsyncClientOnEndpoint(TestJAXBApi.class, TestJAXBAsyncApi.class, "http://mock"); + } + +} diff --git a/core/src/test/java/org/jclouds/rest/binders/BindToXMLPayloadTest.java b/core/src/test/java/org/jclouds/rest/binders/BindToXMLPayloadTest.java index 7111b09f79..926086f079 100644 --- a/core/src/test/java/org/jclouds/rest/binders/BindToXMLPayloadTest.java +++ b/core/src/test/java/org/jclouds/rest/binders/BindToXMLPayloadTest.java @@ -20,8 +20,9 @@ package org.jclouds.rest.binders; import static org.testng.Assert.assertEquals; +import javax.xml.bind.annotation.XmlRootElement; + import org.jclouds.http.HttpRequest; -import org.jclouds.rest.internal.RestAnnotationProcessorTest.TestJAXBDomain; import org.jclouds.xml.XMLParser; import org.jclouds.xml.internal.JAXBParser; import org.testng.annotations.Test; @@ -48,7 +49,8 @@ public class BindToXMLPayloadTest { HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").build(); request = binder.bindToRequest(request, obj); - assertEquals(request.getPayload().getRawContent(), XMLParser.DEFAULT_XML_HEADER + "\n\n Hello World\n\n"); + assertEquals(request.getPayload().getRawContent(), XMLParser.DEFAULT_XML_HEADER + + "\n\n Hello World\n\n"); assertEquals(request.getPayload().getContentMetadata().getContentType(), "application/xml"); } @@ -63,11 +65,11 @@ public class BindToXMLPayloadTest { // Add the unknown content-type header to verify it is changed by the // binder Multimap headers = ImmutableMultimap. of("Content-type", "application/unknown"); - HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").headers(headers) - .build(); + HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").headers(headers).build(); request = binder.bindToRequest(request, obj); - assertEquals(request.getPayload().getRawContent(), XMLParser.DEFAULT_XML_HEADER + "\n\n Hello World\n\n"); + assertEquals(request.getPayload().getRawContent(), XMLParser.DEFAULT_XML_HEADER + + "\n\n Hello World\n\n"); assertEquals(request.getPayload().getContentMetadata().getContentType(), "application/xml"); } @@ -83,4 +85,18 @@ public class BindToXMLPayloadTest { HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").build(); request = binder.bindToRequest(request, new Object()); } + + @XmlRootElement(name = "test") + public static class TestJAXBDomain { + private String elem; + + public String getElem() { + return elem; + } + + public void setElem(String elem) { + this.elem = elem; + } + + } } 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 691ac86504..28b58230a6 100644 --- a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java @@ -2437,87 +2437,6 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { assertEquals(form, "x-amz-copy-source=/eggs/robot"); } - public interface TestJAXBResponseParser { - @GET - @Path("/jaxb/annotation") - @JAXBResponseParser - public ListenableFuture jaxbGetWithAnnotation(); - - @GET - @Path("/jaxb/header") - @Consumes("application/xml") - public ListenableFuture jaxbGetWithAcceptHeader(); - } - - @XmlRootElement(name = "test") - public static class TestJAXBDomain { - private String elem; - - public String getElem() { - return elem; - } - - public void setElem(String elem) { - this.elem = elem; - } - } - - @Test - public void testCreateJAXBResponseParserWithAnnotation() throws SecurityException, NoSuchMethodException { - RestAnnotationProcessor processor = factory(TestJAXBResponseParser.class); - Method method = TestJAXBResponseParser.class.getMethod("jaxbGetWithAnnotation"); - GeneratedHttpRequest request = GeneratedHttpRequest.builder().method("GET").endpoint("http://localhost") - .declaring(TestJAXBResponseParser.class).javaMethod(method).args(new Object[] {}).build(); - Function transformer = processor.createResponseParser(method, request); - assertEquals(transformer.getClass(), ParseXMLWithJAXB.class); - } - - @Test - public void testCreateJAXBResponseParserWithAcceptHeader() throws SecurityException, NoSuchMethodException { - RestAnnotationProcessor processor = factory(TestJAXBResponseParser.class); - Method method = TestJAXBResponseParser.class.getMethod("jaxbGetWithAcceptHeader"); - GeneratedHttpRequest request = GeneratedHttpRequest.builder().method("GET").endpoint("http://localhost") - .declaring(TestJAXBResponseParser.class).javaMethod(method).args(new Object[] {}).build(); - Function transformer = processor.createResponseParser(method, request); - assertEquals(transformer.getClass(), ParseXMLWithJAXB.class); - } - - @SuppressWarnings("unchecked") - @Test - public void testJAXBResponseParserWithAnnotation() throws SecurityException, NoSuchMethodException, IOException { - Method method = TestJAXBResponseParser.class.getMethod("jaxbGetWithAnnotation"); - HttpRequest request = factory(TestJAXBResponseParser.class).createRequest(method); - - assertResponseParserClassEquals(method, request, ParseXMLWithJAXB.class); - // now test that it works! - - Function parser = (Function) RestAnnotationProcessor - .createResponseParser(parserFactory, injector, method, request); - - StringBuilder payload = new StringBuilder(XMLParser.DEFAULT_XML_HEADER); - payload.append("Hello World"); - TestJAXBDomain domain = parser.apply(HttpResponse.builder().statusCode(200).message("ok").payload(payload.toString()).build()); - assertEquals(domain.getElem(), "Hello World"); - } - - @SuppressWarnings("unchecked") - @Test - public void testJAXBResponseParserWithAcceptHeader() throws SecurityException, NoSuchMethodException, IOException { - Method method = TestJAXBResponseParser.class.getMethod("jaxbGetWithAcceptHeader"); - HttpRequest request = factory(TestJAXBResponseParser.class).createRequest(method); - - assertResponseParserClassEquals(method, request, ParseXMLWithJAXB.class); - // now test that it works! - - Function parser = (Function) RestAnnotationProcessor - .createResponseParser(parserFactory, injector, method, request); - - StringBuilder payload = new StringBuilder(XMLParser.DEFAULT_XML_HEADER); - payload.append("Hello World"); - TestJAXBDomain domain = parser.apply(HttpResponse.builder().statusCode(200).message("ok").payload(payload.toString()).build()); - assertEquals(domain.getElem(), "Hello World"); - } - @Test(expectedExceptions = NullPointerException.class) public void testAddHostNullWithHost() throws Exception { assertNull(RestAnnotationProcessor.addHostIfMissing(null, null));