Browse Source

Makes body parameter type explicit

Feign has `MethodMetadata.bodyType()`, but never passed it to encoders.
Encoders that register type adapters need to do so based on the
interface desired as opposed to the implementation class. This change
breaks api compatibility for < 8.x, by requiring an additional arg
on `Encoder.encode`.

see https://github.com/square/retrofit/issues/713
pull/155/head
Adrian Cole 10 years ago
parent
commit
183a5a119c
  1. 1
      CHANGELOG.md
  2. 1
      core/src/main/java/feign/MethodMetadata.java
  3. 4
      core/src/main/java/feign/ReflectiveFeign.java
  4. 5
      core/src/main/java/feign/Types.java
  5. 16
      core/src/main/java/feign/codec/Encoder.java
  6. 8
      core/src/test/java/feign/DefaultContractTest.java
  7. 7
      core/src/test/java/feign/FeignBuilderTest.java
  8. 29
      core/src/test/java/feign/FeignTest.java
  9. 7
      core/src/test/java/feign/assertj/RequestTemplateAssert.java
  10. 6
      core/src/test/java/feign/codec/DefaultEncoderTest.java
  11. 5
      gson/src/main/java/feign/gson/GsonEncoder.java
  12. 32
      gson/src/test/java/feign/gson/GsonCodecTest.java
  13. 7
      jackson/src/main/java/feign/jackson/JacksonEncoder.java
  14. 56
      jackson/src/test/java/feign/jackson/JacksonCodecTest.java
  15. 154
      jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java
  16. 35
      jaxb/src/main/java/feign/jaxb/JAXBDecoder.java
  17. 32
      jaxb/src/main/java/feign/jaxb/JAXBEncoder.java
  18. 218
      jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java
  19. 70
      jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java
  20. 217
      jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java
  21. 82
      jaxb/src/test/java/feign/jaxb/examples/IAMExample.java
  22. 3
      jaxb/src/test/java/feign/jaxb/examples/package-info.java
  23. 10
      jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java

1
CHANGELOG.md

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
### Version 8.0
* Removes Dagger 1.x Dependency
* Removes support for parameters annotated with `javax.inject.@Named`. Use `feign.@Param` instead.
* Makes body parameter type explicit.
### Version 7.1
* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.

1
core/src/main/java/feign/MethodMetadata.java

@ -79,6 +79,7 @@ public final class MethodMetadata implements Serializable { @@ -79,6 +79,7 @@ public final class MethodMetadata implements Serializable {
return this;
}
/** Type corresponding to {@link #bodyIndex()}. */
public Type bodyType() {
return bodyType;
}

4
core/src/main/java/feign/ReflectiveFeign.java

@ -201,7 +201,7 @@ public class ReflectiveFeign extends Feign { @@ -201,7 +201,7 @@ public class ReflectiveFeign extends Feign {
formVariables.put(entry.getKey(), entry.getValue());
}
try {
encoder.encode(formVariables, mutable);
encoder.encode(formVariables, Types.MAP_STRING_WILDCARD, mutable);
} catch (EncodeException e) {
throw e;
} catch (RuntimeException e) {
@ -224,7 +224,7 @@ public class ReflectiveFeign extends Feign { @@ -224,7 +224,7 @@ public class ReflectiveFeign extends Feign {
Object body = argv[metadata.bodyIndex()];
checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
try {
encoder.encode(body, mutable);
encoder.encode(body, metadata.bodyType(), mutable);
} catch (EncodeException e) {
throw e;
} catch (RuntimeException e) {

5
core/src/main/java/feign/Types.java

@ -23,6 +23,7 @@ import java.lang.reflect.Type; @@ -23,6 +23,7 @@ import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Arrays;
import java.util.Map;
import java.util.NoSuchElementException;
/**
@ -32,6 +33,10 @@ import java.util.NoSuchElementException; @@ -32,6 +33,10 @@ import java.util.NoSuchElementException;
* @author Jesse Wilson
*/
final class Types {
/** Type literal for {@code Map<String, ?>}. */
static final Type MAP_STRING_WILDCARD = new ParameterizedTypeImpl(null, Map.class, String.class,
new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { }));
private static final Type[] EMPTY_TYPE_ARRAY = new Type[0];
private Types() {

16
core/src/main/java/feign/codec/Encoder.java

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package feign.codec;
import feign.RequestTemplate;
import java.lang.reflect.Type;
import static java.lang.String.format;
@ -40,8 +41,8 @@ import static java.lang.String.format; @@ -40,8 +41,8 @@ import static java.lang.String.format;
* }
*
* &#064;Override
* public void encode(Object object, RequestTemplate template) {
* template.body(gson.toJson(object));
* public void encode(Object object, Type bodyType, RequestTemplate template) {
* template.body(gson.toJson(object, bodyType));
* }
* }
* </pre>
@ -59,24 +60,25 @@ import static java.lang.String.format; @@ -59,24 +60,25 @@ import static java.lang.String.format;
* </pre>
*/
public interface Encoder {
/**
* Converts objects to an appropriate representation in the template.
*
* @param object what to encode as the request body.
* @param bodyType the type the object should be encoded as. {@code Map<String, ?>}, if form encoding.
* @param template the request template to populate.
* @throws EncodeException when encoding failed due to a checked exception.
*/
void encode(Object object, RequestTemplate template) throws EncodeException;
void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
/**
* Default implementation of {@code Encoder}.
*/
class Default implements Encoder {
@Override
public void encode(Object object, RequestTemplate template) throws EncodeException {
if (object instanceof String) {
@Override public void encode(Object object, Type bodyType, RequestTemplate template) {
if (bodyType == String.class) {
template.body(object.toString());
} else if (object instanceof byte[]) {
} else if (bodyType == byte[].class) {
template.body((byte[]) object, null);
} else if (object != null) {
throw new EncodeException(format("%s is not a type supported by this encoder.", object.getClass()));

8
core/src/test/java/feign/DefaultContractTest.java

@ -231,6 +231,14 @@ public class DefaultContractTest { @@ -231,6 +231,14 @@ public class DefaultContractTest {
);
}
/** Body type is only for the body param. */
@Test public void formParamsDoesNotSetBodyType() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class,
String.class, String.class));
assertThat(md.bodyType()).isNull();
}
interface HeaderParams {
@RequestLine("POST /")
@Headers({"Auth-Token: {Auth-Token}", "Auth-Token: Foo"})

7
core/src/test/java/feign/FeignBuilderTest.java

@ -18,10 +18,7 @@ package feign; @@ -18,10 +18,7 @@ package feign;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import feign.codec.Decoder;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import org.junit.Rule;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
@ -29,6 +26,7 @@ import java.util.Arrays; @@ -29,6 +26,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Rule;
import org.junit.Test;
import static feign.assertj.MockWebServerAssertions.assertThat;
@ -63,8 +61,7 @@ public class FeignBuilderTest { @@ -63,8 +61,7 @@ public class FeignBuilderTest {
String url = "http://localhost:" + server.getPort();
Encoder encoder = new Encoder() {
@Override
public void encode(Object object, RequestTemplate template) throws EncodeException {
@Override public void encode(Object object, Type bodyType, RequestTemplate template) {
template.body(object.toString());
}
};

29
core/src/test/java/feign/FeignTest.java

@ -16,11 +16,13 @@ @@ -16,11 +16,13 @@
package feign;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.codec.StringDecoder;
@ -31,6 +33,7 @@ import java.util.Arrays; @@ -31,6 +33,7 @@ import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@ -137,6 +140,26 @@ public class FeignTest { @@ -137,6 +140,26 @@ public class FeignTest {
.hasBody("[netflix, denominator, password]");
}
/** The type of a parameter value may not be the desired type to encode as. Prefer the interface type. */
@Test public void bodyTypeCorrespondsWithParameterType() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
final AtomicReference<Type> encodedType = new AtomicReference<Type>();
TestInterface api = new TestInterfaceBuilder()
.encoder(new Encoder.Default() {
@Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
encodedType.set(bodyType);
}
})
.target("http://localhost:" + server.getPort());
api.body(Arrays.asList("netflix", "denominator", "password"));
server.takeRequest();
assertThat(encodedType.get()).isEqualTo(new TypeToken<List<String>>(){}.getType());
}
@Test public void postGZIPEncodedBodyParam() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
@ -348,7 +371,7 @@ public class FeignTest { @@ -348,7 +371,7 @@ public class FeignTest {
private final Feign.Builder delegate = new Feign.Builder()
.decoder(new Decoder.Default())
.encoder(new Encoder() {
@Override public void encode(Object object, RequestTemplate template) {
@Override public void encode(Object object, Type bodyType, RequestTemplate template) {
if (object instanceof Map) {
template.body(new Gson().toJson(object));
} else {
@ -362,8 +385,8 @@ public class FeignTest { @@ -362,8 +385,8 @@ public class FeignTest {
return this;
}
TestInterfaceBuilder client(Client client) {
delegate.client(client);
TestInterfaceBuilder encoder(Encoder encoder) {
delegate.encoder(encoder);
return this;
}

7
core/src/test/java/feign/assertj/RequestTemplateAssert.java

@ -46,7 +46,12 @@ public final class RequestTemplateAssert extends AbstractAssert<RequestTemplateA @@ -46,7 +46,12 @@ public final class RequestTemplateAssert extends AbstractAssert<RequestTemplateA
}
public RequestTemplateAssert hasBody(String utf8Expected) {
return hasBody(utf8Expected.getBytes(UTF_8));
isNotNull();
if (actual.bodyTemplate() != null) {
failWithMessage("\nExpecting bodyTemplate to be null, but was:<%s>", actual.bodyTemplate());
}
objects.assertEqual(info, new String(actual.body(), UTF_8), utf8Expected);
return this;
}
public RequestTemplateAssert hasBody(byte[] expected) {

6
core/src/test/java/feign/codec/DefaultEncoderTest.java

@ -34,14 +34,14 @@ public class DefaultEncoderTest { @@ -34,14 +34,14 @@ public class DefaultEncoderTest {
@Test public void testEncodesStrings() throws Exception {
String content = "This is my content";
RequestTemplate template = new RequestTemplate();
encoder.encode(content, template);
encoder.encode(content, String.class, template);
assertEquals(content, new String(template.body(), UTF_8));
}
@Test public void testEncodesByteArray() throws Exception {
byte[] content = {12, 34, 56};
RequestTemplate template = new RequestTemplate();
encoder.encode(content, template);
encoder.encode(content, byte[].class, template);
assertTrue(Arrays.equals(content, template.body()));
}
@ -49,6 +49,6 @@ public class DefaultEncoderTest { @@ -49,6 +49,6 @@ public class DefaultEncoderTest {
thrown.expect(EncodeException.class);
thrown.expectMessage("is not a type supported by this encoder.");
encoder.encode(new Date(), new RequestTemplate());
encoder.encode(new Date(), Date.class, new RequestTemplate());
}
}

5
gson/src/main/java/feign/gson/GsonEncoder.java

@ -19,6 +19,7 @@ import com.google.gson.Gson; @@ -19,6 +19,7 @@ import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import feign.RequestTemplate;
import feign.codec.Encoder;
import java.lang.reflect.Type;
import java.util.Collections;
public class GsonEncoder implements Encoder {
@ -36,7 +37,7 @@ public class GsonEncoder implements Encoder { @@ -36,7 +37,7 @@ public class GsonEncoder implements Encoder {
this.gson = gson;
}
@Override public void encode(Object object, RequestTemplate template) {
template.body(gson.toJson(object));
@Override public void encode(Object object, Type bodyType, RequestTemplate template) {
template.body(gson.toJson(object, bodyType));
}
}

32
gson/src/test/java/feign/gson/GsonCodecTest.java

@ -43,7 +43,7 @@ public class GsonCodecTest { @@ -43,7 +43,7 @@ public class GsonCodecTest {
map.put("foo", 1);
RequestTemplate template = new RequestTemplate();
new GsonEncoder().encode(map, template);
new GsonEncoder().encode(map, map.getClass(), template);
assertThat(template).hasBody("" //
+ "{\n" //
@ -68,7 +68,7 @@ public class GsonCodecTest { @@ -68,7 +68,7 @@ public class GsonCodecTest {
form.put("bar", Arrays.asList(2, 3));
RequestTemplate template = new RequestTemplate();
new GsonEncoder().encode(form, template);
new GsonEncoder().encode(form, new TypeToken<Map<String, ?>>(){}.getType(), template);
assertThat(template).hasBody("" //
+ "{\n" //
@ -129,7 +129,11 @@ public class GsonCodecTest { @@ -129,7 +129,11 @@ public class GsonCodecTest {
final TypeAdapter upperZone = new TypeAdapter<Zone>() {
@Override public void write(JsonWriter out, Zone value) throws IOException {
throw new IllegalArgumentException();
out.beginObject();
for(Map.Entry<String, Object> entry : value.entrySet()) {
out.name(entry.getKey()).value(entry.getValue().toString().toUpperCase());
}
out.endObject();
}
@Override public Zone read(JsonReader in) throws IOException {
@ -155,4 +159,26 @@ public class GsonCodecTest { @@ -155,4 +159,26 @@ public class GsonCodecTest {
assertEquals(zones, decoder.decode(response, new TypeToken<List<Zone>>() {
}.getType()));
}
@Test public void customEncoder() throws Exception {
GsonEncoder encoder = new GsonEncoder(Arrays.<TypeAdapter<?>>asList(upperZone));
List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("denominator.io."));
zones.add(new Zone("denominator.io.", "abcd"));
RequestTemplate template = new RequestTemplate();
encoder.encode(zones, new TypeToken<List<Zone>>(){}.getType(), template);
assertThat(template).hasBody("" //
+ "[\n" //
+ " {\n" //
+ " \"name\": \"DENOMINATOR.IO.\"\n" //
+ " },\n" //
+ " {\n" //
+ " \"name\": \"DENOMINATOR.IO.\",\n" //
+ " \"id\": \"ABCD\"\n" //
+ " }\n" //
+ "]");
}
}

7
jackson/src/main/java/feign/jackson/JacksonEncoder.java

@ -17,12 +17,14 @@ package feign.jackson; @@ -17,12 +17,14 @@ package feign.jackson;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import java.lang.reflect.Type;
import java.util.Collections;
public class JacksonEncoder implements Encoder {
@ -43,9 +45,10 @@ public class JacksonEncoder implements Encoder { @@ -43,9 +45,10 @@ public class JacksonEncoder implements Encoder {
this.mapper = mapper;
}
@Override public void encode(Object object, RequestTemplate template) throws EncodeException {
@Override public void encode(Object object, Type bodyType, RequestTemplate template) {
try {
template.body(mapper.writeValueAsString(object));
JavaType javaType = mapper.getTypeFactory().constructType(bodyType);
template.body(mapper.writerWithType(javaType).writeValueAsString(object));
} catch (JsonProcessingException e) {
throw new EncodeException(e.getMessage(), e);
}

56
jackson/src/test/java/feign/jackson/JacksonCodecTest.java

@ -1,12 +1,15 @@ @@ -1,12 +1,15 @@
package feign.jackson;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import feign.RequestTemplate;
import feign.Response;
import java.io.IOException;
@ -31,7 +34,7 @@ public class JacksonCodecTest { @@ -31,7 +34,7 @@ public class JacksonCodecTest {
map.put("foo", 1);
RequestTemplate template = new RequestTemplate();
new JacksonEncoder().encode(map, template);
new JacksonEncoder().encode(map, map.getClass(), template);
assertThat(template).hasBody(""//
+ "{\n" //
@ -45,7 +48,8 @@ public class JacksonCodecTest { @@ -45,7 +48,8 @@ public class JacksonCodecTest {
form.put("bar", Arrays.asList(2, 3));
RequestTemplate template = new RequestTemplate();
new JacksonEncoder().encode(form, template);
new JacksonEncoder().encode(form, new TypeReference<Map<String, ?>>() {
}.getType(), template);
assertThat(template).hasBody(""//
+ "{\n" //
@ -120,14 +124,9 @@ public class JacksonCodecTest { @@ -120,14 +124,9 @@ public class JacksonCodecTest {
}
}
static class ZoneModule extends SimpleModule {
public ZoneModule() {
addDeserializer(Zone.class, new ZoneDeserializer());
}
}
@Test public void customDecoder() throws Exception {
JacksonDecoder decoder = new JacksonDecoder(Arrays.<Module>asList(new ZoneModule()));
JacksonDecoder decoder = new JacksonDecoder(
Arrays.<Module>asList(new SimpleModule().addDeserializer(Zone.class, new ZoneDeserializer())));
List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("DENOMINATOR.IO."));
@ -135,7 +134,42 @@ public class JacksonCodecTest { @@ -135,7 +134,42 @@ public class JacksonCodecTest {
Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(zones, decoder.decode(response, new TypeReference<List<Zone>>() {
}.getType()));
assertEquals(zones, decoder.decode(response, new TypeReference<List<Zone>>(){}.getType()));
}
static class ZoneSerializer extends StdSerializer<Zone> {
public ZoneSerializer() {
super(Zone.class);
}
@Override public void serialize(Zone value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeStartObject();
for(Map.Entry<String, Object> entry : value.entrySet()) {
jgen.writeFieldName(entry.getKey());
jgen.writeString(entry.getValue().toString().toUpperCase());
}
jgen.writeEndObject();
}
}
@Test public void customEncoder() throws Exception {
JacksonEncoder encoder = new JacksonEncoder(
Arrays.<Module>asList(new SimpleModule().addSerializer(Zone.class, new ZoneSerializer())));
List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("denominator.io."));
zones.add(new Zone("denominator.io.", "abcd"));
RequestTemplate template = new RequestTemplate();
encoder.encode(zones, new TypeReference<List<Zone>>(){}.getType(), template);
assertThat(template).hasBody("" //
+ "[ {\n"
+ " \"name\" : \"DENOMINATOR.IO.\"\n"
+ "}, {\n"
+ " \"name\" : \"DENOMINATOR.IO.\",\n"
+ " \"id\" : \"ABCD\"\n"
+ "} ]");
}
}

154
jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java

@ -29,100 +29,100 @@ import java.util.concurrent.ConcurrentHashMap; @@ -29,100 +29,100 @@ import java.util.concurrent.ConcurrentHashMap;
* Creates and caches JAXB contexts as well as creates Marshallers and Unmarshallers for each context.
*/
public final class JAXBContextFactory {
private final ConcurrentHashMap<Class, JAXBContext> jaxbContexts = new ConcurrentHashMap<Class, JAXBContext>(64);
private final Map<String, Object> properties;
private final ConcurrentHashMap<Class, JAXBContext> jaxbContexts = new ConcurrentHashMap<Class, JAXBContext>(64);
private final Map<String, Object> properties;
private JAXBContextFactory(Map<String, Object> properties) {
this.properties = properties;
private JAXBContextFactory(Map<String, Object> properties) {
this.properties = properties;
}
/**
* Creates a new {@link javax.xml.bind.Unmarshaller} that handles the supplied class.
*/
public Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
JAXBContext ctx = getContext(clazz);
return ctx.createUnmarshaller();
}
/**
* Creates a new {@link javax.xml.bind.Marshaller} that handles the supplied class.
*/
public Marshaller createMarshaller(Class<?> clazz) throws JAXBException {
JAXBContext ctx = getContext(clazz);
Marshaller marshaller = ctx.createMarshaller();
setMarshallerProperties(marshaller);
return marshaller;
}
private void setMarshallerProperties(Marshaller marshaller) throws PropertyException {
Iterator<String> keys = properties.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
marshaller.setProperty(key, properties.get(key));
}
}
private JAXBContext getContext(Class<?> clazz) throws JAXBException {
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
if (jaxbContext == null) {
jaxbContext = JAXBContext.newInstance(clazz);
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
}
return jaxbContext;
}
/**
* Creates instances of {@link feign.jaxb.JAXBContextFactory}
*/
public static class Builder {
private final Map<String, Object> properties = new HashMap<String, Object>(5);
/**
* Creates a new {@link javax.xml.bind.Unmarshaller} that handles the supplied class.
* Sets the jaxb.encoding property of any Marshaller created by this factory.
*/
public Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
JAXBContext ctx = getContext(clazz);
return ctx.createUnmarshaller();
public Builder withMarshallerJAXBEncoding(String value) {
properties.put(Marshaller.JAXB_ENCODING, value);
return this;
}
/**
* Creates a new {@link javax.xml.bind.Marshaller} that handles the supplied class.
* Sets the jaxb.schemaLocation property of any Marshaller created by this factory.
*/
public Marshaller createMarshaller(Class<?> clazz) throws JAXBException {
JAXBContext ctx = getContext(clazz);
Marshaller marshaller = ctx.createMarshaller();
setMarshallerProperties(marshaller);
return marshaller;
public Builder withMarshallerSchemaLocation(String value) {
properties.put(Marshaller.JAXB_SCHEMA_LOCATION, value);
return this;
}
private void setMarshallerProperties(Marshaller marshaller) throws PropertyException {
Iterator<String> keys = properties.keySet().iterator();
while(keys.hasNext()) {
String key = keys.next();
marshaller.setProperty(key, properties.get(key));
}
/**
* Sets the jaxb.noNamespaceSchemaLocation property of any Marshaller created by this factory.
*/
public Builder withMarshallerNoNamespaceSchemaLocation(String value) {
properties.put(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, value);
return this;
}
private JAXBContext getContext(Class<?> clazz) throws JAXBException {
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
if (jaxbContext == null) {
jaxbContext = JAXBContext.newInstance(clazz);
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
}
return jaxbContext;
/**
* Sets the jaxb.formatted.output property of any Marshaller created by this factory.
*/
public Builder withMarshallerFormattedOutput(Boolean value) {
properties.put(Marshaller.JAXB_FORMATTED_OUTPUT, value);
return this;
}
/**
* Creates instances of {@link feign.jaxb.JAXBContextFactory}
* Sets the jaxb.fragment property of any Marshaller created by this factory.
*/
public static class Builder {
private final Map<String, Object> properties = new HashMap<String, Object>(5);
/**
* Sets the jaxb.encoding property of any Marshaller created by this factory.
*/
public Builder withMarshallerJAXBEncoding(String value) {
properties.put(Marshaller.JAXB_ENCODING, value);
return this;
}
/**
* Sets the jaxb.schemaLocation property of any Marshaller created by this factory.
*/
public Builder withMarshallerSchemaLocation(String value) {
properties.put(Marshaller.JAXB_SCHEMA_LOCATION, value);
return this;
}
/**
* Sets the jaxb.noNamespaceSchemaLocation property of any Marshaller created by this factory.
*/
public Builder withMarshallerNoNamespaceSchemaLocation(String value) {
properties.put(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, value);
return this;
}
/**
* Sets the jaxb.formatted.output property of any Marshaller created by this factory.
*/
public Builder withMarshallerFormattedOutput(Boolean value) {
properties.put(Marshaller.JAXB_FORMATTED_OUTPUT, value);
return this;
}
/**
* Sets the jaxb.fragment property of any Marshaller created by this factory.
*/
public Builder withMarshallerFragment(Boolean value) {
properties.put(Marshaller.JAXB_FRAGMENT, value);
return this;
}
public Builder withMarshallerFragment(Boolean value) {
properties.put(Marshaller.JAXB_FRAGMENT, value);
return this;
}
/**
* Creates a new {@link feign.jaxb.JAXBContextFactory} instance.
*/
public JAXBContextFactory build() {
return new JAXBContextFactory(properties);
}
/**
* Creates a new {@link feign.jaxb.JAXBContextFactory} instance.
*/
public JAXBContextFactory build() {
return new JAXBContextFactory(properties);
}
}
}

35
jaxb/src/main/java/feign/jaxb/JAXBDecoder.java

@ -15,7 +15,6 @@ @@ -15,7 +15,6 @@
*/
package feign.jaxb;
import feign.FeignException;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
@ -45,23 +44,25 @@ import javax.xml.bind.Unmarshaller; @@ -45,23 +44,25 @@ import javax.xml.bind.Unmarshaller;
* </p>
*/
public class JAXBDecoder implements Decoder {
private final JAXBContextFactory jaxbContextFactory;
private final JAXBContextFactory jaxbContextFactory;
public JAXBDecoder(JAXBContextFactory jaxbContextFactory) {
this.jaxbContextFactory = jaxbContextFactory;
}
public JAXBDecoder(JAXBContextFactory jaxbContextFactory) {
this.jaxbContextFactory = jaxbContextFactory;
}
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
try {
Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class) type);
return unmarshaller.unmarshal(response.body().asInputStream());
} catch (JAXBException e) {
throw new DecodeException(e.toString(), e);
} finally {
if(response.body() != null) {
response.body().close();
}
}
@Override public Object decode(Response response, Type type) throws IOException {
if (!(type instanceof Class)) {
throw new UnsupportedOperationException("JAXB only supports decoding raw types. Found " + type);
}
try {
Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class) type);
return unmarshaller.unmarshal(response.body().asInputStream());
} catch (JAXBException e) {
throw new DecodeException(e.toString(), e);
} finally {
if (response.body() != null) {
response.body().close();
}
}
}
}

32
jaxb/src/main/java/feign/jaxb/JAXBEncoder.java

@ -19,6 +19,8 @@ import feign.RequestTemplate; @@ -19,6 +19,8 @@ import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import java.io.StringWriter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
@ -43,21 +45,23 @@ import javax.xml.bind.Marshaller; @@ -43,21 +45,23 @@ import javax.xml.bind.Marshaller;
* </p>
*/
public class JAXBEncoder implements Encoder {
private final JAXBContextFactory jaxbContextFactory;
private final JAXBContextFactory jaxbContextFactory;
public JAXBEncoder(JAXBContextFactory jaxbContextFactory) {
this.jaxbContextFactory = jaxbContextFactory;
}
public JAXBEncoder(JAXBContextFactory jaxbContextFactory) {
this.jaxbContextFactory = jaxbContextFactory;
}
@Override
public void encode(Object object, RequestTemplate template) throws EncodeException {
try {
Marshaller marshaller = jaxbContextFactory.createMarshaller(object.getClass());
StringWriter stringWriter = new StringWriter();
marshaller.marshal(object, stringWriter);
template.body(stringWriter.toString());
} catch (JAXBException e) {
throw new EncodeException(e.toString(), e);
}
@Override public void encode(Object object, Type bodyType, RequestTemplate template) {
if (!(bodyType instanceof Class)) {
throw new UnsupportedOperationException("JAXB only supports encoding raw types. Found " + bodyType);
}
try {
Marshaller marshaller = jaxbContextFactory.createMarshaller((Class) bodyType);
StringWriter stringWriter = new StringWriter();
marshaller.marshal(object, stringWriter);
template.body(stringWriter.toString());
} catch (JAXBException e) {
throw new EncodeException(e.toString(), e);
}
}
}

218
jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java

@ -18,148 +18,170 @@ package feign.jaxb; @@ -18,148 +18,170 @@ package feign.jaxb;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Encoder;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static feign.Util.UTF_8;
import static feign.assertj.FeignAssertions.assertThat;
import static org.junit.Assert.assertEquals;
public class JAXBCodecTest {
@Rule public final ExpectedException thrown = ExpectedException.none();
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
static class MockObject {
@XmlElement
private String value;
@Override
public boolean equals(Object obj) {
if (obj instanceof MockObject) {
MockObject other = (MockObject) obj;
return value.equals(other.value);
}
return false;
}
@Override
public int hashCode() {
return value != null ? value.hashCode() : 0;
}
}
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) static class MockObject {
@Test
public void encodesXml() throws Exception {
MockObject mock = new MockObject();
mock.value = "Test";
@XmlElement private String value;
RequestTemplate template = new RequestTemplate();
new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(mock, template);
@Override public boolean equals(Object obj) {
if (obj instanceof MockObject) {
MockObject other = (MockObject) obj;
return value.equals(other.value);
}
return false;
}
assertThat(template).hasBody(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject><value>Test</value></mockObject>");
@Override public int hashCode() {
return value != null ? value.hashCode() : 0;
}
}
@Test
public void encodesXmlWithCustomJAXBEncoding() throws Exception {
JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder()
.withMarshallerJAXBEncoding("UTF-16")
.build();
@Test public void encodesXml() throws Exception {
MockObject mock = new MockObject();
mock.value = "Test";
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
RequestTemplate template = new RequestTemplate();
new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(mock, MockObject.class, template);
MockObject mock = new MockObject();
mock.value = "Test";
assertThat(template).hasBody(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject><value>Test</value></mockObject>");
}
RequestTemplate template = new RequestTemplate();
encoder.encode(mock, template);
@Test public void doesntEncodeParameterizedTypes() throws Exception {
thrown.expect(UnsupportedOperationException.class);
thrown.expectMessage("JAXB only supports encoding raw types. Found java.util.Map<java.lang.String, ?>");
assertThat(template).hasBody("<?xml version=\"1.0\" encoding=\"UTF-16\" "
+ "standalone=\"yes\"?><mockObject><value>Test</value></mockObject>");
class ParameterizedHolder {
Map<String, ?> field;
}
Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType();
@Test
public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception {
JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder()
.withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
.build();
RequestTemplate template = new RequestTemplate();
new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(Collections.emptyMap(), parameterized, template);
}
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
@Test public void encodesXmlWithCustomJAXBEncoding() throws Exception {
JAXBContextFactory jaxbContextFactory =
new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build();
MockObject mock = new MockObject();
mock.value = "Test";
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
RequestTemplate template = new RequestTemplate();
encoder.encode(mock, template);
MockObject mock = new MockObject();
mock.value = "Test";
assertThat(template).hasBody("<?xml version=\"1.0\" encoding=\"UTF-8\" " +
"standalone=\"yes\"?><mockObject xsi:schemaLocation=\"http://apihost " +
"http://apihost/schema.xsd\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<value>Test</value></mockObject>");
}
RequestTemplate template = new RequestTemplate();
encoder.encode(mock, MockObject.class, template);
@Test
public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception {
JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder()
.withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd")
.build();
assertThat(template).hasBody("<?xml version=\"1.0\" encoding=\"UTF-16\" "
+ "standalone=\"yes\"?><mockObject><value>Test</value></mockObject>");
}
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
@Test public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception {
JAXBContextFactory jaxbContextFactory =
new JAXBContextFactory.Builder().withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
.build();
MockObject mock = new MockObject();
mock.value = "Test";
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
RequestTemplate template = new RequestTemplate();
encoder.encode(mock, template);
MockObject mock = new MockObject();
mock.value = "Test";
assertThat(template).hasBody("<?xml version=\"1.0\" encoding=\"UTF-8\" " +
"standalone=\"yes\"?><mockObject xsi:noNamespaceSchemaLocation=\"http://apihost/schema.xsd\" " +
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<value>Test</value></mockObject>");
}
RequestTemplate template = new RequestTemplate();
encoder.encode(mock, MockObject.class, template);
@Test
public void encodesXmlWithCustomJAXBFormattedOutput() {
JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder()
.withMarshallerFormattedOutput(true)
.build();
assertThat(template).hasBody("<?xml version=\"1.0\" encoding=\"UTF-8\" " +
"standalone=\"yes\"?><mockObject xsi:schemaLocation=\"http://apihost " +
"http://apihost/schema.xsd\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<value>Test</value></mockObject>");
}
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
@Test public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception {
JAXBContextFactory jaxbContextFactory =
new JAXBContextFactory.Builder().withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build();
MockObject mock = new MockObject();
mock.value = "Test";
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
RequestTemplate template = new RequestTemplate();
encoder.encode(mock, template);
MockObject mock = new MockObject();
mock.value = "Test";
String NEWLINE = System.getProperty("line.separator");
RequestTemplate template = new RequestTemplate();
encoder.encode(mock, MockObject.class, template);
assertThat(template).hasBody(new StringBuilder()
.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>").append(NEWLINE)
.append("<mockObject>").append(NEWLINE)
.append(" <value>Test</value>").append(NEWLINE)
.append("</mockObject>").append(NEWLINE).toString());
}
assertThat(template).hasBody("<?xml version=\"1.0\" encoding=\"UTF-8\" " +
"standalone=\"yes\"?><mockObject xsi:noNamespaceSchemaLocation=\"http://apihost/schema.xsd\" " +
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<value>Test</value></mockObject>");
}
@Test public void encodesXmlWithCustomJAXBFormattedOutput() {
JAXBContextFactory jaxbContextFactory =
new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build();
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
MockObject mock = new MockObject();
mock.value = "Test";
@Test
public void decodesXml() throws Exception {
MockObject mock = new MockObject();
mock.value = "Test";
RequestTemplate template = new RequestTemplate();
encoder.encode(mock, MockObject.class, template);
String mockXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject>" +
"<value>Test</value></mockObject>";
String NEWLINE = System.getProperty("line.separator");
Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), mockXml, UTF_8);
assertThat(template).hasBody(
new StringBuilder().append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>")
.append(NEWLINE)
.append("<mockObject>")
.append(NEWLINE)
.append(" <value>Test</value>")
.append(NEWLINE)
.append("</mockObject>")
.append(NEWLINE)
.toString());
}
JAXBDecoder decoder = new JAXBDecoder(new JAXBContextFactory.Builder().build());
@Test public void decodesXml() throws Exception {
MockObject mock = new MockObject();
mock.value = "Test";
assertEquals(mock, decoder.decode(response, MockObject.class));
String mockXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject>"
+ "<value>Test</value></mockObject>";
Response response = Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), mockXml, UTF_8);
JAXBDecoder decoder = new JAXBDecoder(new JAXBContextFactory.Builder().build());
assertEquals(mock, decoder.decode(response, MockObject.class));
}
@Test public void doesntDecodeParameterizedTypes() throws Exception {
thrown.expect(UnsupportedOperationException.class);
thrown.expectMessage("JAXB only supports decoding raw types. Found java.util.Map<java.lang.String, ?>");
class ParameterizedHolder {
Map<String, ?> field;
}
Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType();
Response response = Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), "<foo/>", UTF_8);
new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, parameterized);
}
}

70
jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java

@ -22,55 +22,41 @@ import static org.junit.Assert.assertEquals; @@ -22,55 +22,41 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class JAXBContextFactoryTest {
@Test
public void buildsMarshallerWithJAXBEncodingProperty() throws Exception {
JAXBContextFactory factory = new JAXBContextFactory.Builder()
.withMarshallerJAXBEncoding("UTF-16")
.build();
@Test public void buildsMarshallerWithJAXBEncodingProperty() throws Exception {
JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build();
Marshaller marshaller = factory.createMarshaller(Object.class);
assertEquals("UTF-16", marshaller.getProperty(Marshaller.JAXB_ENCODING));
}
Marshaller marshaller = factory.createMarshaller(Object.class);
assertEquals("UTF-16", marshaller.getProperty(Marshaller.JAXB_ENCODING));
}
@Test
public void buildsMarshallerWithSchemaLocationProperty() throws Exception {
JAXBContextFactory factory = new JAXBContextFactory.Builder()
.withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
.build();
@Test public void buildsMarshallerWithSchemaLocationProperty() throws Exception {
JAXBContextFactory factory =
new JAXBContextFactory.Builder().withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
.build();
Marshaller marshaller = factory.createMarshaller(Object.class);
assertEquals("http://apihost http://apihost/schema.xsd",
marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION));
}
Marshaller marshaller = factory.createMarshaller(Object.class);
assertEquals("http://apihost http://apihost/schema.xsd", marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION));
}
@Test
public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Exception {
JAXBContextFactory factory = new JAXBContextFactory.Builder()
.withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd")
.build();
@Test public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Exception {
JAXBContextFactory factory =
new JAXBContextFactory.Builder().withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build();
Marshaller marshaller = factory.createMarshaller(Object.class);
assertEquals("http://apihost/schema.xsd",
marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION));
}
Marshaller marshaller = factory.createMarshaller(Object.class);
assertEquals("http://apihost/schema.xsd", marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION));
}
@Test
public void buildsMarshallerWithFormattedOutputProperty() throws Exception {
JAXBContextFactory factory = new JAXBContextFactory.Builder()
.withMarshallerFormattedOutput(true)
.build();
@Test public void buildsMarshallerWithFormattedOutputProperty() throws Exception {
JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build();
Marshaller marshaller = factory.createMarshaller(Object.class);
assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FORMATTED_OUTPUT));
}
Marshaller marshaller = factory.createMarshaller(Object.class);
assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FORMATTED_OUTPUT));
}
@Test
public void buildsMarshallerWithFragmentProperty() throws Exception {
JAXBContextFactory factory = new JAXBContextFactory.Builder()
.withMarshallerFragment(true)
.build();
@Test public void buildsMarshallerWithFragmentProperty() throws Exception {
JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerFragment(true).build();
Marshaller marshaller = factory.createMarshaller(Object.class);
assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FRAGMENT));
}
Marshaller marshaller = factory.createMarshaller(Object.class);
assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FRAGMENT));
}
}

217
jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java

@ -30,133 +30,132 @@ import static feign.Util.UTF_8; @@ -30,133 +30,132 @@ import static feign.Util.UTF_8;
// http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
public class AWSSignatureVersion4 {
String region = "us-east-1";
String service = "iam";
String accessKey;
String secretKey;
public AWSSignatureVersion4(String accessKey, String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}
public Request apply(RequestTemplate input) {
if (!input.headers().isEmpty()) throw new UnsupportedOperationException("headers not supported");
if (input.body() != null) throw new UnsupportedOperationException("body not supported");
String host = URI.create(input.url()).getHost();
String timestamp;
synchronized (iso8601) {
timestamp = iso8601.format(new Date());
}
String credentialScope = String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request");
input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
input.query("X-Amz-Credential", accessKey + "/" + credentialScope);
input.query("X-Amz-Date", timestamp);
input.query("X-Amz-SignedHeaders", "host");
input.header("Host", host);
String canonicalString = canonicalString(input, host);
String toSign = toSign(timestamp, credentialScope, canonicalString);
String region = "us-east-1";
String service = "iam";
String accessKey;
String secretKey;
byte[] signatureKey = signatureKey(secretKey, timestamp);
String signature = hex(hmacSHA256(toSign, signatureKey));
public AWSSignatureVersion4(String accessKey, String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}
input.query("X-Amz-Signature", signature);
public Request apply(RequestTemplate input) {
if (!input.headers().isEmpty()) throw new UnsupportedOperationException("headers not supported");
if (input.body() != null) throw new UnsupportedOperationException("body not supported");
return input.request();
}
String host = URI.create(input.url()).getHost();
byte[] signatureKey(String secretKey, String timestamp) {
byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8);
byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret);
byte[] kRegion = hmacSHA256(region, kDate);
byte[] kService = hmacSHA256(service, kRegion);
byte[] kSigning = hmacSHA256("aws4_request", kService);
return kSigning;
String timestamp;
synchronized (iso8601) {
timestamp = iso8601.format(new Date());
}
static byte[] hmacSHA256(String data, byte[] key) {
try {
String algorithm = "HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data.getBytes(UTF_8));
} catch (Exception e) {
throw new RuntimeException(e);
}
String credentialScope = String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request");
input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
input.query("X-Amz-Credential", accessKey + "/" + credentialScope);
input.query("X-Amz-Date", timestamp);
input.query("X-Amz-SignedHeaders", "host");
input.header("Host", host);
String canonicalString = canonicalString(input, host);
String toSign = toSign(timestamp, credentialScope, canonicalString);
byte[] signatureKey = signatureKey(secretKey, timestamp);
String signature = hex(hmacSHA256(toSign, signatureKey));
input.query("X-Amz-Signature", signature);
return input.request();
}
byte[] signatureKey(String secretKey, String timestamp) {
byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8);
byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret);
byte[] kRegion = hmacSHA256(region, kDate);
byte[] kService = hmacSHA256(service, kRegion);
byte[] kSigning = hmacSHA256("aws4_request", kService);
return kSigning;
}
static byte[] hmacSHA256(String data, byte[] key) {
try {
String algorithm = "HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data.getBytes(UTF_8));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
private static String canonicalString(RequestTemplate input, String host) {
StringBuilder canonicalRequest = new StringBuilder();
// HTTPRequestMethod + '\n' +
canonicalRequest.append(input.method()).append('\n');
private static String canonicalString(RequestTemplate input, String host) {
StringBuilder canonicalRequest = new StringBuilder();
// HTTPRequestMethod + '\n' +
canonicalRequest.append(input.method()).append('\n');
// CanonicalURI + '\n' +
canonicalRequest.append(URI.create(input.url()).getPath()).append('\n');
// CanonicalURI + '\n' +
canonicalRequest.append(URI.create(input.url()).getPath()).append('\n');
// CanonicalQueryString + '\n' +
canonicalRequest.append(input.queryLine().substring(1));
canonicalRequest.append('\n');
// CanonicalQueryString + '\n' +
canonicalRequest.append(input.queryLine().substring(1));
canonicalRequest.append('\n');
// CanonicalHeaders + '\n' +
canonicalRequest.append("host:").append(host).append('\n');
// CanonicalHeaders + '\n' +
canonicalRequest.append("host:").append(host).append('\n');
canonicalRequest.append('\n');
canonicalRequest.append('\n');
// SignedHeaders + '\n' +
canonicalRequest.append("host").append('\n');
// SignedHeaders + '\n' +
canonicalRequest.append("host").append('\n');
// HexEncode(Hash(Payload))
String bodyText =
input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null;
if (bodyText != null) {
canonicalRequest.append(hex(sha256(bodyText)));
} else {
canonicalRequest.append(EMPTY_STRING_HASH);
}
return canonicalRequest.toString();
// HexEncode(Hash(Payload))
String bodyText =
input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null;
if (bodyText != null) {
canonicalRequest.append(hex(sha256(bodyText)));
} else {
canonicalRequest.append(EMPTY_STRING_HASH);
}
private static String toSign(String timestamp, String credentialScope, String canonicalRequest) {
StringBuilder toSign = new StringBuilder();
// Algorithm + '\n' +
toSign.append("AWS4-HMAC-SHA256").append('\n');
// RequestDate + '\n' +
toSign.append(timestamp).append('\n');
// CredentialScope + '\n' +
toSign.append(credentialScope).append('\n');
// HexEncode(Hash(CanonicalRequest))
toSign.append(hex(sha256(canonicalRequest)));
return toSign.toString();
return canonicalRequest.toString();
}
private static String toSign(String timestamp, String credentialScope, String canonicalRequest) {
StringBuilder toSign = new StringBuilder();
// Algorithm + '\n' +
toSign.append("AWS4-HMAC-SHA256").append('\n');
// RequestDate + '\n' +
toSign.append(timestamp).append('\n');
// CredentialScope + '\n' +
toSign.append(credentialScope).append('\n');
// HexEncode(Hash(CanonicalRequest))
toSign.append(hex(sha256(canonicalRequest)));
return toSign.toString();
}
private static String hex(byte[] data) {
StringBuilder result = new StringBuilder(data.length * 2);
for (byte b : data) {
result.append(String.format("%02x", b & 0xff));
}
private static String hex(byte[] data) {
StringBuilder result = new StringBuilder(data.length * 2);
for (byte b : data) {
result.append(String.format("%02x", b & 0xff));
}
return result.toString();
return result.toString();
}
static byte[] sha256(String data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(data.getBytes(UTF_8));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
static byte[] sha256(String data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(data.getBytes(UTF_8));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
static {
iso8601.setTimeZone(TimeZone.getTimeZone("GMT"));
}
static {
iso8601.setTimeZone(TimeZone.getTimeZone("GMT"));
}
}

82
jaxb/src/test/java/feign/jaxb/examples/IAMExample.java

@ -30,61 +30,53 @@ import javax.xml.bind.annotation.XmlType; @@ -30,61 +30,53 @@ import javax.xml.bind.annotation.XmlType;
public class IAMExample {
interface IAM {
@RequestLine("GET /?Action=GetUser&Version=2010-05-08") GetUserResponse userResponse();
}
public static void main(String... args) {
IAM iam = Feign.builder()
.decoder(new JAXBDecoder(new JAXBContextFactory.Builder().build()))
.target(new IAMTarget(args[0], args[1]));
GetUserResponse response = iam.userResponse();
System.out.println("UserId: " + response.result.user.id);
}
static class IAMTarget extends AWSSignatureVersion4 implements Target<IAM> {
interface IAM {
@RequestLine("GET /?Action=GetUser&Version=2010-05-08") GetUserResponse userResponse();
}
@Override public Class<IAM> type() {
return IAM.class;
}
public static void main(String... args) {
IAM iam = Feign.builder()
.decoder(new JAXBDecoder(new JAXBContextFactory.Builder().build()))
.target(new IAMTarget(args[0], args[1]));
@Override public String name() {
return "iam";
}
GetUserResponse response = iam.userResponse();
System.out.println("UserId: " + response.result.user.id);
}
@Override public String url() {
return "https://iam.amazonaws.com";
}
static class IAMTarget extends AWSSignatureVersion4 implements Target<IAM> {
private IAMTarget(String accessKey, String secretKey) {
super(accessKey, secretKey);
}
@Override public Class<IAM> type() {
return IAM.class;
}
@Override public Request apply(RequestTemplate in) {
in.insert(0, url());
return super.apply(in);
}
@Override public String name() {
return "iam";
}
@XmlRootElement(name = "GetUserResponse", namespace = "https://iam.amazonaws.com/doc/2010-05-08/")
@XmlAccessorType(XmlAccessType.FIELD)
static class GetUserResponse {
@XmlElement(name = "GetUserResult")
private GetUserResult result;
@Override public String url() {
return "https://iam.amazonaws.com";
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "GetUserResult")
static class GetUserResult {
@XmlElement(name = "User")
private User user;
private IAMTarget(String accessKey, String secretKey) {
super(accessKey, secretKey);
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "User")
static class User {
@XmlElement(name = "UserId")
private String id;
@Override public Request apply(RequestTemplate in) {
in.insert(0, url());
return super.apply(in);
}
}
@XmlRootElement(name = "GetUserResponse", namespace = "https://iam.amazonaws.com/doc/2010-05-08/")
@XmlAccessorType(XmlAccessType.FIELD) static class GetUserResponse {
@XmlElement(name = "GetUserResult") private GetUserResult result;
}
@XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "GetUserResult") static class GetUserResult {
@XmlElement(name = "User") private User user;
}
@XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "User") static class User {
@XmlElement(name = "UserId") private String id;
}
}

3
jaxb/src/test/java/feign/jaxb/examples/package-info.java

@ -13,5 +13,4 @@ @@ -13,5 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@javax.xml.bind.annotation.XmlSchema(namespace = "https://iam.amazonaws.com/doc/2010-05-08/", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
package feign.jaxb.examples;
@javax.xml.bind.annotation.XmlSchema(namespace = "https://iam.amazonaws.com/doc/2010-05-08/", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) package feign.jaxb.examples;

10
jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java

@ -44,7 +44,7 @@ import static java.util.Arrays.asList; @@ -44,7 +44,7 @@ import static java.util.Arrays.asList;
import static org.assertj.core.data.MapEntry.entry;
/**
* Tests interfaces defined per {@link feign.jaxrs.JAXRSContract} are interpreted into expected {@link feign
* Tests interfaces defined per {@link JAXRSContract} are interpreted into expected {@link feign
* .RequestTemplate template}
* instances.
*/
@ -332,6 +332,14 @@ public class JAXRSContractTest { @@ -332,6 +332,14 @@ public class JAXRSContractTest {
);
}
/** Body type is only for the body param. */
@Test public void formParamsDoesNotSetBodyType() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class,
String.class, String.class));
assertThat(md.bodyType()).isNull();
}
@Test public void emptyFormParam() throws Exception {
thrown.expect(IllegalStateException.class);
thrown.expectMessage("FormParam.value() was empty on parameter 0");

Loading…
Cancel
Save