diff --git a/spring-web/src/main/java/org/springframework/http/codec/cbor/Jackson2CborDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/cbor/Jackson2CborDecoder.java new file mode 100644 index 00000000000..8d045c36b5b --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/cbor/Jackson2CborDecoder.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.cbor; + +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.AbstractJackson2Decoder; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Decode bytes into CBOR and convert to Object's with Jackson. + * Stream decoding is not supported yet. + * + * @author Sebastien Deleuze + * @since 5.2 + * @see Jackson2CborEncoder + * @see Add CBOR support to WebFlux + */ +public class Jackson2CborDecoder extends AbstractJackson2Decoder { + + public Jackson2CborDecoder() { + this(Jackson2ObjectMapperBuilder.cbor().build(), new MediaType("application", "cbor")); + } + + public Jackson2CborDecoder(ObjectMapper mapper, MimeType... mimeTypes) { + super(mapper, mimeTypes); + Assert.isAssignable(CBORFactory.class, mapper.getFactory().getClass()); + } + + @Override + public Flux decode(Publisher input, ResolvableType elementType, MimeType mimeType, Map hints) { + throw new UnsupportedOperationException("Does not support stream decoding yet"); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/cbor/Jackson2CborEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/cbor/Jackson2CborEncoder.java new file mode 100644 index 00000000000..f5449be6c61 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/cbor/Jackson2CborEncoder.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.cbor; + +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.AbstractJackson2Encoder; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Encode from an {@code Object} to bytes of CBOR objects using Jackson. + * Stream encoding is not supported yet. + * + * @author Sebastien Deleuze + * @since 5.2 + * @see Jackson2CborDecoder + * @see Add CBOR support to WebFlux + */ +public class Jackson2CborEncoder extends AbstractJackson2Encoder { + + public Jackson2CborEncoder() { + this(Jackson2ObjectMapperBuilder.cbor().build(), new MediaType("application", "cbor")); + } + + public Jackson2CborEncoder(ObjectMapper mapper, MimeType... mimeTypes) { + super(mapper, mimeTypes); + Assert.isAssignable(CBORFactory.class, mapper.getFactory().getClass()); + } + + @Override + public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map hints) { + throw new UnsupportedOperationException("Does not support stream encoding yet"); + } +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/cbor/Jackson2CborDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/cbor/Jackson2CborDecoderTests.java new file mode 100644 index 00000000000..04350796662 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/cbor/Jackson2CborDecoderTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.cbor; + +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.AbstractDecoderTestCase; +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 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 Jackson2CborDecoder}. + * + * @author Sebastien Deleuze + */ +public class Jackson2CborDecoderTests extends AbstractDecoderTestCase { + + private final static MimeType CBOR_MIME_TYPE = new MimeType("application", "cbor"); + + private Pojo pojo1 = new Pojo("f1", "b1"); + + private Pojo pojo2 = new Pojo("f2", "b2"); + + private ObjectMapper mapper = Jackson2ObjectMapperBuilder.cbor().build(); + + public Jackson2CborDecoderTests() { + super(new Jackson2CborDecoder()); + } + + @Override + @Test + public void canDecode() { + assertTrue(decoder.canDecode(forClass(Pojo.class), CBOR_MIME_TYPE)); + assertTrue(decoder.canDecode(forClass(Pojo.class), null)); + + assertFalse(decoder.canDecode(forClass(String.class), null)); + assertFalse(decoder.canDecode(forClass(Pojo.class), APPLICATION_JSON)); + } + + @Override + @Test(expected = UnsupportedOperationException.class) + public void decode() { + Flux input = Flux.just(this.pojo1, this.pojo2) + .map(this::writeObject) + .flatMap(this::dataBuffer); + + testDecodeAll(input, Pojo.class, step -> step + .expectNext(pojo1) + .expectNext(pojo2) + .verifyComplete()); + + } + + private byte[] writeObject(Object o) { + try { + return this.mapper.writer().writeValueAsBytes(o); + } + catch (JsonProcessingException e) { + throw new AssertionError(e); + } + + } + + @Override + public void decodeToMono() { + List expected = Arrays.asList(pojo1, pojo2); + + Flux input = Flux.just(expected) + .map(this::writeObject) + .flatMap(this::dataBuffer); + + ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class); + testDecodeToMono(input, elementType, step -> step + .expectNext(expected) + .expectComplete() + .verify(), null, null); + } +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/cbor/Jackson2CborEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/cbor/Jackson2CborEncoderTests.java new file mode 100644 index 00000000000..2f9d8190f16 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/cbor/Jackson2CborEncoderTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.cbor; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractLeakCheckingTestCase; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.io.buffer.DataBufferUtils.release; +import static org.springframework.http.MediaType.APPLICATION_XML; + +/** + * Unit tests for {@link Jackson2CborEncoder}. + * + * @author Sebastien Deleuze + */ +public class Jackson2CborEncoderTests extends AbstractLeakCheckingTestCase { + + private final static MimeType CBOR_MIME_TYPE = new MimeType("application", "cbor"); + + private final ObjectMapper mapper = Jackson2ObjectMapperBuilder.cbor().build(); + + private final Jackson2CborEncoder encoder = new Jackson2CborEncoder(); + + private Consumer pojoConsumer(Pojo expected) { + return dataBuffer -> { + try { + Pojo actual = this.mapper.reader().forType(Pojo.class) + .readValue(DataBufferTestUtils.dumpBytes(dataBuffer)); + assertEquals(expected, actual); + release(dataBuffer); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }; + } + + @Test + public void canEncode() { + ResolvableType pojoType = ResolvableType.forClass(Pojo.class); + assertTrue(this.encoder.canEncode(pojoType, CBOR_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, CBOR_MIME_TYPE)); + } + + @Test + public void encode() { + Pojo value = new Pojo("foo", "bar"); + DataBuffer result = encoder.encodeValue(value, this.bufferFactory, ResolvableType.forClass(Pojo.class), CBOR_MIME_TYPE, null); + pojoConsumer(value).accept(result); + } + + @Test(expected = UnsupportedOperationException.class) + public void encodeStream() { + Pojo pojo1 = new Pojo("foo", "bar"); + Pojo pojo2 = new Pojo("foofoo", "barbar"); + Pojo pojo3 = new Pojo("foofoofoo", "barbarbar"); + Flux input = Flux.just(pojo1, pojo2, pojo3); + ResolvableType type = ResolvableType.forClass(Pojo.class); + encoder.encode(input, this.bufferFactory, type, CBOR_MIME_TYPE, null); + } +}