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.
This commit is contained in:
Ignasi Barrera 2013-01-03 13:56:55 +01:00
parent 0b6b980adb
commit f1d81eaf12
5 changed files with 200 additions and 90 deletions

View File

@ -24,6 +24,10 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.Target; 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 * Shows the transformer type used to parse XML with the
* {@link ParseXMLWithJAXB} parser in a HttpResponse. * {@link ParseXMLWithJAXB} parser in a HttpResponse.
@ -34,4 +38,10 @@ import java.lang.annotation.Target;
@Retention(RUNTIME) @Retention(RUNTIME)
public @interface JAXBResponseParser { 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;
} }

View File

@ -67,6 +67,7 @@ import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.lang.model.type.NullType;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam; import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam; import javax.ws.rs.HeaderParam;
@ -815,10 +816,16 @@ public abstract class RestAnnotationProcessor {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static Key<? extends Function<HttpResponse, ?>> getJAXBParserKeyForMethod(Method method) { public static Key<? extends Function<HttpResponse, ?>> getJAXBParserKeyForMethod(Method method) {
Type returnVal = getReturnTypeForMethod(method); Optional<Type> configuredReturnVal = Optional.absent();
Type parserType = Types.newParameterizedType(ParseXMLWithJAXB.class, returnVal); if (method.isAnnotationPresent(JAXBResponseParser.class)) {
return (Key<? extends Function<HttpResponse, ?>>) Key.get(parserType); Type configuredClass = method.getAnnotation(JAXBResponseParser.class).value();
} configuredReturnVal = configuredClass.equals(NullType.class) ? Optional.<Type> absent() : Optional
.<Type> of(configuredClass);
}
Type returnVal = configuredReturnVal.or(getReturnTypeForMethod(method));
Type parserType = Types.newParameterizedType(ParseXMLWithJAXB.class, returnVal);
return (Key<? extends Function<HttpResponse, ?>>) Key.get(parserType);
}
public static Type getReturnTypeForMethod(Method method) { public static Type getReturnTypeForMethod(Method method) {
Type returnVal; Type returnVal;

View File

@ -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<JAXBResponseParserAnnotationExpectTest.TestJAXBApi> {
@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<TestJAXBDomain> jaxbGetWithAnnotation();
@GET
@Path("/jaxb/custom")
@JAXBResponseParser(TestJAXBDomain.class)
public ListenableFuture<Object> jaxbGetWithAnnotationAndCustomClass();
@GET
@Path("/jaxb/header")
@Consumes(MediaType.APPLICATION_XML)
public ListenableFuture<TestJAXBDomain> jaxbGetWithAcceptHeader();
@GET
@Path("/jaxb/transformer")
@JAXBResponseParser(TestJAXBDomain.class)
@Transform(ToString.class)
public ListenableFuture<String> jaxbGetWithTransformer();
}
private static class ToString implements Function<Object, String> {
@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("<test><elem>Hello World</elem></test>").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("<test><elem>Hello World</elem></test>").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("<test><elem>Hello World</elem></test>").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("<test><elem>Hello World</elem></test>").build());
String result = api.jaxbGetWithTransformer();
assertEquals(result, "TestJAXBDomain [elem=Hello World]");
}
@Override
public ProviderMetadata createProviderMetadata() {
return forClientMappedToAsyncClientOnEndpoint(TestJAXBApi.class, TestJAXBAsyncApi.class, "http://mock");
}
}

View File

@ -20,8 +20,9 @@ package org.jclouds.rest.binders;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import javax.xml.bind.annotation.XmlRootElement;
import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequest;
import org.jclouds.rest.internal.RestAnnotationProcessorTest.TestJAXBDomain;
import org.jclouds.xml.XMLParser; import org.jclouds.xml.XMLParser;
import org.jclouds.xml.internal.JAXBParser; import org.jclouds.xml.internal.JAXBParser;
import org.testng.annotations.Test; import org.testng.annotations.Test;
@ -48,7 +49,8 @@ public class BindToXMLPayloadTest {
HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").build(); HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").build();
request = binder.bindToRequest(request, obj); request = binder.bindToRequest(request, obj);
assertEquals(request.getPayload().getRawContent(), XMLParser.DEFAULT_XML_HEADER + "\n<test>\n <elem>Hello World</elem>\n</test>\n"); assertEquals(request.getPayload().getRawContent(), XMLParser.DEFAULT_XML_HEADER
+ "\n<test>\n <elem>Hello World</elem>\n</test>\n");
assertEquals(request.getPayload().getContentMetadata().getContentType(), "application/xml"); 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 // Add the unknown content-type header to verify it is changed by the
// binder // binder
Multimap<String, String> headers = ImmutableMultimap.<String, String> of("Content-type", "application/unknown"); Multimap<String, String> headers = ImmutableMultimap.<String, String> of("Content-type", "application/unknown");
HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").headers(headers) HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").headers(headers).build();
.build();
request = binder.bindToRequest(request, obj); request = binder.bindToRequest(request, obj);
assertEquals(request.getPayload().getRawContent(), XMLParser.DEFAULT_XML_HEADER + "\n<test>\n <elem>Hello World</elem>\n</test>\n"); assertEquals(request.getPayload().getRawContent(), XMLParser.DEFAULT_XML_HEADER
+ "\n<test>\n <elem>Hello World</elem>\n</test>\n");
assertEquals(request.getPayload().getContentMetadata().getContentType(), "application/xml"); assertEquals(request.getPayload().getContentMetadata().getContentType(), "application/xml");
} }
@ -83,4 +85,18 @@ public class BindToXMLPayloadTest {
HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").build(); HttpRequest request = HttpRequest.builder().method("GET").endpoint("http://momma").build();
request = binder.bindToRequest(request, new Object()); 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;
}
}
} }

View File

@ -2437,87 +2437,6 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest {
assertEquals(form, "x-amz-copy-source=/eggs/robot"); assertEquals(form, "x-amz-copy-source=/eggs/robot");
} }
public interface TestJAXBResponseParser {
@GET
@Path("/jaxb/annotation")
@JAXBResponseParser
public ListenableFuture<TestJAXBDomain> jaxbGetWithAnnotation();
@GET
@Path("/jaxb/header")
@Consumes("application/xml")
public ListenableFuture<TestJAXBDomain> 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<HttpResponse, ?> 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<HttpResponse, ?> 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<HttpResponse, TestJAXBDomain> parser = (Function<HttpResponse, TestJAXBDomain>) RestAnnotationProcessor
.createResponseParser(parserFactory, injector, method, request);
StringBuilder payload = new StringBuilder(XMLParser.DEFAULT_XML_HEADER);
payload.append("<test><elem>Hello World</elem></test>");
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<HttpResponse, TestJAXBDomain> parser = (Function<HttpResponse, TestJAXBDomain>) RestAnnotationProcessor
.createResponseParser(parserFactory, injector, method, request);
StringBuilder payload = new StringBuilder(XMLParser.DEFAULT_XML_HEADER);
payload.append("<test><elem>Hello World</elem></test>");
TestJAXBDomain domain = parser.apply(HttpResponse.builder().statusCode(200).message("ok").payload(payload.toString()).build());
assertEquals(domain.getElem(), "Hello World");
}
@Test(expectedExceptions = NullPointerException.class) @Test(expectedExceptions = NullPointerException.class)
public void testAddHostNullWithHost() throws Exception { public void testAddHostNullWithHost() throws Exception {
assertNull(RestAnnotationProcessor.addHostIfMissing(null, null)); assertNull(RestAnnotationProcessor.addHostIfMissing(null, null));