Browse Source
This binary format more efficient than JSON should be useful for server to server communication, for example in micro-services use cases. Issue: SPR-15424pull/1476/head
20 changed files with 749 additions and 277 deletions
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.http.codec.json; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.UncheckedIOException; |
||||
import java.lang.annotation.Annotation; |
||||
import java.util.Map; |
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory; |
||||
import com.fasterxml.jackson.core.JsonParser; |
||||
import com.fasterxml.jackson.core.JsonProcessingException; |
||||
import com.fasterxml.jackson.databind.JavaType; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import com.fasterxml.jackson.databind.ObjectReader; |
||||
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; |
||||
import com.fasterxml.jackson.databind.util.TokenBuffer; |
||||
import org.reactivestreams.Publisher; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.codec.CodecException; |
||||
import org.springframework.core.codec.DecodingException; |
||||
import org.springframework.core.io.buffer.DataBuffer; |
||||
import org.springframework.http.codec.HttpMessageDecoder; |
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.http.server.reactive.ServerHttpResponse; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
/** |
||||
* Base class providing support methods for Jackson 2.9 decoding. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
*/ |
||||
public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport implements HttpMessageDecoder<Object> { |
||||
|
||||
/** |
||||
* Constructor with a Jackson {@link ObjectMapper} to use. |
||||
*/ |
||||
protected AbstractJackson2Decoder(ObjectMapper mapper, MimeType... mimeTypes) { |
||||
super(mapper, mimeTypes); |
||||
} |
||||
|
||||
@Override |
||||
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { |
||||
JavaType javaType = objectMapper().getTypeFactory().constructType(elementType.getType()); |
||||
// Skip String: CharSequenceDecoder + "*/*" comes after
|
||||
return (!CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) && |
||||
objectMapper().canDeserialize(javaType) && supportsMimeType(mimeType)); |
||||
} |
||||
|
||||
@Override |
||||
public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType, |
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { |
||||
|
||||
Flux<TokenBuffer> tokens = tokenize(input, true); |
||||
return decodeInternal(tokens, elementType, mimeType, hints); |
||||
} |
||||
|
||||
@Override |
||||
public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType, |
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { |
||||
|
||||
Flux<TokenBuffer> tokens = tokenize(input, false); |
||||
return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty(); |
||||
} |
||||
|
||||
private Flux<TokenBuffer> tokenize(Publisher<DataBuffer> input, boolean tokenizeArrayElements) { |
||||
try { |
||||
JsonFactory factory = objectMapper().getFactory(); |
||||
JsonParser nonBlockingParser = factory.createNonBlockingByteArrayParser(); |
||||
Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(nonBlockingParser, |
||||
tokenizeArrayElements); |
||||
return Flux.from(input) |
||||
.flatMap(tokenizer) |
||||
.doFinally(t -> tokenizer.endOfInput()); |
||||
} |
||||
catch (IOException ex) { |
||||
return Flux.error(new UncheckedIOException(ex)); |
||||
} |
||||
|
||||
} |
||||
|
||||
private Flux<Object> decodeInternal(Flux<TokenBuffer> tokens, |
||||
ResolvableType elementType, @Nullable MimeType mimeType, |
||||
@Nullable Map<String, Object> hints) { |
||||
|
||||
Assert.notNull(tokens, "'tokens' must not be null"); |
||||
Assert.notNull(elementType, "'elementType' must not be null"); |
||||
|
||||
MethodParameter param = getParameter(elementType); |
||||
Class<?> contextClass = (param != null ? param.getContainingClass() : null); |
||||
JavaType javaType = getJavaType(elementType.getType(), contextClass); |
||||
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); |
||||
|
||||
ObjectReader reader = (jsonView != null ? |
||||
objectMapper().readerWithView(jsonView).forType(javaType) : |
||||
objectMapper().readerFor(javaType)); |
||||
|
||||
return tokens.map(tokenBuffer -> { |
||||
try { |
||||
return reader.readValue(tokenBuffer.asParser()); |
||||
} |
||||
catch (InvalidDefinitionException ex) { |
||||
throw new CodecException("Type definition error: " + ex.getType(), ex); |
||||
} |
||||
catch (JsonProcessingException ex) { |
||||
throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex); |
||||
} |
||||
catch (IOException ex) { |
||||
throw new DecodingException("I/O error while parsing input stream", ex); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
|
||||
// HttpMessageDecoder...
|
||||
|
||||
@Override |
||||
public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType, |
||||
ServerHttpRequest request, ServerHttpResponse response) { |
||||
|
||||
return getHints(actualType); |
||||
} |
||||
|
||||
@Override |
||||
protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) { |
||||
return parameter.getParameterAnnotation(annotType); |
||||
} |
||||
} |
||||
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.http.codec.json; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.OutputStream; |
||||
import java.lang.annotation.Annotation; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException; |
||||
import com.fasterxml.jackson.databind.JavaType; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import com.fasterxml.jackson.databind.ObjectWriter; |
||||
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; |
||||
import org.reactivestreams.Publisher; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.codec.CodecException; |
||||
import org.springframework.core.codec.EncodingException; |
||||
import org.springframework.core.io.buffer.DataBuffer; |
||||
import org.springframework.core.io.buffer.DataBufferFactory; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.codec.HttpMessageEncoder; |
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.http.server.reactive.ServerHttpResponse; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
/** |
||||
* Base class providing support methods for Jackson 2.9 encoding. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @author Arjen Poutsma |
||||
* @since 5.0 |
||||
*/ |
||||
public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport implements HttpMessageEncoder<Object> { |
||||
|
||||
protected final List<MediaType> streamingMediaTypes = new ArrayList<>(1); |
||||
|
||||
/** |
||||
* Constructor with a Jackson {@link ObjectMapper} to use. |
||||
*/ |
||||
protected AbstractJackson2Encoder(ObjectMapper mapper, MimeType... mimeTypes) { |
||||
super(mapper, mimeTypes); |
||||
} |
||||
|
||||
/** |
||||
* Configure "streaming" media types for which flushing should be performed |
||||
* automatically vs at the end of the stream. |
||||
* <p>By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}. |
||||
* @param mediaTypes one or more media types to add to the list |
||||
* @see HttpMessageEncoder#getStreamingMediaTypes() |
||||
*/ |
||||
public void setStreamingMediaTypes(List<MediaType> mediaTypes) { |
||||
this.streamingMediaTypes.clear(); |
||||
this.streamingMediaTypes.addAll(mediaTypes); |
||||
} |
||||
|
||||
@Override |
||||
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { |
||||
Class<?> clazz = elementType.resolve(Object.class); |
||||
return (Object.class == clazz) || |
||||
!String.class.isAssignableFrom(elementType.resolve(clazz)) && |
||||
objectMapper().canSerialize(clazz) && supportsMimeType(mimeType); |
||||
} |
||||
|
||||
@Override |
||||
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory, |
||||
ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { |
||||
|
||||
Assert.notNull(inputStream, "'inputStream' must not be null"); |
||||
Assert.notNull(bufferFactory, "'bufferFactory' must not be null"); |
||||
Assert.notNull(elementType, "'elementType' must not be null"); |
||||
|
||||
if (inputStream instanceof Mono) { |
||||
return Flux.from(inputStream).map(value -> |
||||
encodeValue(value, mimeType, bufferFactory, elementType, hints)); |
||||
} |
||||
else if (this.streamingMediaTypes.stream().anyMatch(streamingMediaType -> streamingMediaType.isCompatibleWith(mimeType))) { |
||||
return Flux.from(inputStream).map(value -> { |
||||
DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints); |
||||
buffer.write(new byte[]{'\n'}); |
||||
return buffer; |
||||
}); |
||||
} |
||||
else { |
||||
ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); |
||||
return Flux.from(inputStream).collectList().map(list -> |
||||
encodeValue(list, mimeType, bufferFactory, listType, hints)).flux(); |
||||
} |
||||
} |
||||
|
||||
private DataBuffer encodeValue(Object value, @Nullable MimeType mimeType, DataBufferFactory bufferFactory, |
||||
ResolvableType elementType, @Nullable Map<String, Object> hints) { |
||||
|
||||
JavaType javaType = getJavaType(elementType.getType(), null); |
||||
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); |
||||
ObjectWriter writer = (jsonView != null ? |
||||
objectMapper().writerWithView(jsonView) : objectMapper().writer()); |
||||
|
||||
if (javaType.isContainerType()) { |
||||
writer = writer.forType(javaType); |
||||
} |
||||
|
||||
writer = customizeWriter(writer, mimeType, elementType, hints); |
||||
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer(); |
||||
OutputStream outputStream = buffer.asOutputStream(); |
||||
try { |
||||
writer.writeValue(outputStream, value); |
||||
} |
||||
catch (InvalidDefinitionException ex) { |
||||
throw new CodecException("Type definition error: " + ex.getType(), ex); |
||||
} |
||||
catch (JsonProcessingException ex) { |
||||
throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex); |
||||
} |
||||
catch (IOException ex) { |
||||
throw new IllegalStateException("Unexpected I/O error while writing to data buffer", ex); |
||||
} |
||||
|
||||
return buffer; |
||||
} |
||||
|
||||
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType, |
||||
ResolvableType elementType, @Nullable Map<String, Object> hints) { |
||||
return writer; |
||||
} |
||||
|
||||
|
||||
// HttpMessageEncoder...
|
||||
|
||||
@Override |
||||
public List<MediaType> getStreamingMediaTypes() { |
||||
return Collections.unmodifiableList(this.streamingMediaTypes); |
||||
} |
||||
|
||||
@Override |
||||
public Map<String, Object> getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType, |
||||
@Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { |
||||
|
||||
return (actualType != null ? getHints(actualType) : Collections.emptyMap()); |
||||
} |
||||
|
||||
@Override |
||||
protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) { |
||||
return parameter.getMethodAnnotation(annotType); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.http.codec.json; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import com.fasterxml.jackson.dataformat.smile.SmileFactory; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
/** |
||||
* Decode a byte stream into Smile and convert to Object's with Jackson 2.9. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
* @see Jackson2JsonEncoder |
||||
*/ |
||||
public class Jackson2SmileDecoder extends AbstractJackson2Decoder { |
||||
|
||||
private static final MimeType SMILE_MIME_TYPE = new MediaType("application", "x-jackson-smile"); |
||||
|
||||
|
||||
public Jackson2SmileDecoder() { |
||||
this(Jackson2ObjectMapperBuilder.smile().build(), SMILE_MIME_TYPE); |
||||
} |
||||
|
||||
public Jackson2SmileDecoder(ObjectMapper mapper, MimeType... mimeTypes) { |
||||
super(mapper, mimeTypes); |
||||
Assert.isAssignable(SmileFactory.class, mapper.getFactory().getClass()); |
||||
} |
||||
|
||||
@Override |
||||
public List<MimeType> getDecodableMimeTypes() { |
||||
return Arrays.asList(SMILE_MIME_TYPE); |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.http.codec.json; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import com.fasterxml.jackson.dataformat.smile.SmileFactory; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
/** |
||||
* Encode from an {@code Object} stream to a byte stream of Smile objects using Jackson 2.9. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 5.0 |
||||
* @see Jackson2SmileDecoder |
||||
*/ |
||||
public class Jackson2SmileEncoder extends AbstractJackson2Encoder { |
||||
|
||||
private static final MimeType SMILE_MIME_TYPE = new MediaType("application", "x-jackson-smile"); |
||||
|
||||
|
||||
public Jackson2SmileEncoder() { |
||||
this(Jackson2ObjectMapperBuilder.smile().build(), new MediaType("application", "x-jackson-smile")); |
||||
} |
||||
|
||||
public Jackson2SmileEncoder(ObjectMapper mapper, MimeType... mimeTypes) { |
||||
super(mapper, mimeTypes); |
||||
Assert.isAssignable(SmileFactory.class, mapper.getFactory().getClass()); |
||||
this.streamingMediaTypes.add(new MediaType("application", "stream+x-jackson-smile")); |
||||
} |
||||
|
||||
@Override |
||||
public List<MimeType> getEncodableMimeTypes() { |
||||
return Arrays.asList(SMILE_MIME_TYPE); |
||||
} |
||||
} |
||||
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.http.codec.json; |
||||
|
||||
import java.util.List; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
import reactor.test.StepVerifier; |
||||
|
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.codec.CodecException; |
||||
import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; |
||||
import org.springframework.core.io.buffer.DataBuffer; |
||||
import org.springframework.http.codec.Pojo; |
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
import static java.util.Arrays.asList; |
||||
import static java.util.Collections.emptyMap; |
||||
import static org.junit.Assert.assertFalse; |
||||
import static org.junit.Assert.assertTrue; |
||||
import static org.springframework.core.ResolvableType.forClass; |
||||
import static org.springframework.http.MediaType.APPLICATION_JSON; |
||||
|
||||
/** |
||||
* Unit tests for {@link Jackson2SmileDecoder}. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
*/ |
||||
public class Jackson2SmileDecoderTests extends AbstractDataBufferAllocatingTestCase { |
||||
|
||||
private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile"); |
||||
|
||||
private final Jackson2SmileDecoder decoder = new Jackson2SmileDecoder(); |
||||
|
||||
@Test |
||||
public void canDecode() { |
||||
assertTrue(decoder.canDecode(forClass(Pojo.class), SMILE_MIME_TYPE)); |
||||
assertTrue(decoder.canDecode(forClass(Pojo.class), null)); |
||||
|
||||
assertFalse(decoder.canDecode(forClass(String.class), null)); |
||||
assertFalse(decoder.canDecode(forClass(Pojo.class), APPLICATION_JSON)); |
||||
} |
||||
|
||||
@Test |
||||
public void decodePojo() throws Exception { |
||||
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); |
||||
Pojo pojo = new Pojo("foo", "bar"); |
||||
byte[] serializedPojo = mapper.writer().writeValueAsBytes(pojo); |
||||
|
||||
Flux<DataBuffer> source = Flux.just(this.bufferFactory.wrap(serializedPojo)); |
||||
ResolvableType elementType = forClass(Pojo.class); |
||||
Flux<Object> flux = decoder.decode(source, elementType, null, emptyMap()); |
||||
|
||||
StepVerifier.create(flux) |
||||
.expectNext(pojo) |
||||
.verifyComplete(); |
||||
} |
||||
|
||||
@Test |
||||
public void decodePojoWithError() throws Exception { |
||||
Flux<DataBuffer> source = Flux.just(stringBuffer("123")); |
||||
ResolvableType elementType = forClass(Pojo.class); |
||||
Flux<Object> flux = decoder.decode(source, elementType, null, emptyMap()); |
||||
|
||||
StepVerifier.create(flux).verifyError(CodecException.class); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeToList() throws Exception { |
||||
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); |
||||
List<Pojo> list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")); |
||||
byte[] serializedList = mapper.writer().writeValueAsBytes(list); |
||||
Flux<DataBuffer> source = Flux.just(this.bufferFactory.wrap(serializedList)); |
||||
|
||||
ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class); |
||||
Mono<Object> mono = decoder.decodeToMono(source, elementType, null, emptyMap()); |
||||
|
||||
StepVerifier.create(mono) |
||||
.expectNext(list) |
||||
.expectComplete() |
||||
.verify(); |
||||
} |
||||
|
||||
@Test |
||||
public void decodeToFlux() throws Exception { |
||||
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); |
||||
List<Pojo> list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")); |
||||
byte[] serializedList = mapper.writer().writeValueAsBytes(list); |
||||
Flux<DataBuffer> source = Flux.just(this.bufferFactory.wrap(serializedList)); |
||||
|
||||
ResolvableType elementType = forClass(Pojo.class); |
||||
Flux<Object> flux = decoder.decode(source, elementType, null, emptyMap()); |
||||
|
||||
StepVerifier.create(flux) |
||||
.expectNext(new Pojo("f1", "b1")) |
||||
.expectNext(new Pojo("f2", "b2")) |
||||
.verifyComplete(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.http.codec.json; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.UncheckedIOException; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
||||
import com.fasterxml.jackson.annotation.JsonTypeName; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
import reactor.test.StepVerifier; |
||||
|
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; |
||||
import org.springframework.core.io.buffer.DataBuffer; |
||||
import org.springframework.core.io.buffer.DataBufferUtils; |
||||
import org.springframework.core.io.buffer.support.DataBufferTestUtils; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.codec.Pojo; |
||||
import org.springframework.http.codec.ServerSentEvent; |
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
import static java.util.Collections.emptyMap; |
||||
import static org.junit.Assert.assertFalse; |
||||
import static org.junit.Assert.assertTrue; |
||||
import static org.springframework.http.MediaType.APPLICATION_XML; |
||||
|
||||
/** |
||||
* Unit tests for {@link Jackson2SmileEncoder}. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
*/ |
||||
public class Jackson2SmileEncoderTests extends AbstractDataBufferAllocatingTestCase { |
||||
|
||||
private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile"); |
||||
|
||||
private final Jackson2SmileEncoder encoder = new Jackson2SmileEncoder(); |
||||
|
||||
|
||||
@Test |
||||
public void canEncode() { |
||||
ResolvableType pojoType = ResolvableType.forClass(Pojo.class); |
||||
assertTrue(this.encoder.canEncode(pojoType, SMILE_MIME_TYPE)); |
||||
assertTrue(this.encoder.canEncode(pojoType, null)); |
||||
|
||||
// SPR-15464
|
||||
assertTrue(this.encoder.canEncode(ResolvableType.NONE, null)); |
||||
} |
||||
|
||||
@Test |
||||
public void canNotEncode() { |
||||
assertFalse(this.encoder.canEncode(ResolvableType.forClass(String.class), null)); |
||||
assertFalse(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), APPLICATION_XML)); |
||||
|
||||
ResolvableType sseType = ResolvableType.forClass(ServerSentEvent.class); |
||||
assertFalse(this.encoder.canEncode(sseType, SMILE_MIME_TYPE)); |
||||
} |
||||
|
||||
@Test |
||||
public void encode() throws Exception { |
||||
Flux<Pojo> source = Flux.just( |
||||
new Pojo("foo", "bar"), |
||||
new Pojo("foofoo", "barbar"), |
||||
new Pojo("foofoofoo", "barbarbar") |
||||
); |
||||
ResolvableType type = ResolvableType.forClass(Pojo.class); |
||||
Flux<DataBuffer> output = this.encoder.encode(source, this.bufferFactory, type, null, emptyMap()); |
||||
|
||||
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); |
||||
StepVerifier.create(output) |
||||
.consumeNextWith(dataBuffer -> readPojo(mapper, List.class, dataBuffer)) |
||||
.verifyComplete(); |
||||
} |
||||
|
||||
@Test |
||||
public void encodeAsStream() throws Exception { |
||||
Flux<Pojo> source = Flux.just( |
||||
new Pojo("foo", "bar"), |
||||
new Pojo("foofoo", "barbar"), |
||||
new Pojo("foofoofoo", "barbarbar") |
||||
); |
||||
ResolvableType type = ResolvableType.forClass(Pojo.class); |
||||
MediaType mediaType = new MediaType("application", "stream+x-jackson-smile"); |
||||
Flux<DataBuffer> output = this.encoder.encode(source, this.bufferFactory, type, mediaType, emptyMap()); |
||||
|
||||
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); |
||||
StepVerifier.create(output) |
||||
.consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer)) |
||||
.consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer)) |
||||
.consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer)) |
||||
.verifyComplete(); |
||||
} |
||||
|
||||
public <T> T readPojo(ObjectMapper mapper, Class<T> valueType, DataBuffer dataBuffer) { |
||||
try { |
||||
T value = mapper.reader().forType(valueType).readValue(DataBufferTestUtils.dumpBytes(dataBuffer)); |
||||
DataBufferUtils.release(dataBuffer); |
||||
return value; |
||||
} |
||||
catch (IOException ex) { |
||||
throw new UncheckedIOException(ex); |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue