diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java new file mode 100644 index 0000000000..7112dfa793 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2016 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.core.codec; + +import java.util.Map; + +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.DataBufferUtils; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Decoder for {@code byte} arrays. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public class ByteArrayDecoder extends AbstractDecoder { + + public ByteArrayDecoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canDecode(ResolvableType elementType, MimeType mimeType) { + Class clazz = elementType.getRawClass(); + return (super.canDecode(elementType, mimeType) && byte[].class == clazz); + } + + @Override + public Flux decode(Publisher inputStream, ResolvableType elementType, + MimeType mimeType, Map hints) { + + return Flux.from(inputStream).map((dataBuffer) -> { + byte[] result = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(result); + DataBufferUtils.release(dataBuffer); + return result ; + }); + } + + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java new file mode 100644 index 0000000000..23ee1d3be4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2016 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.core.codec; + +import java.util.Map; + +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.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Encoder for {@code byte} arrays. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public class ByteArrayEncoder extends AbstractEncoder { + + public ByteArrayEncoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canEncode(ResolvableType elementType, MimeType mimeType) { + Class clazz = elementType.getRawClass(); + return (super.canEncode(elementType, mimeType) && byte[].class.isAssignableFrom(clazz)); + } + + @Override + public Flux encode(Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, + Map hints) { + + return Flux.from(inputStream).map(bufferFactory::wrap); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java index 1ee2d82673..789335a7b6 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java @@ -52,4 +52,11 @@ public interface DataBufferFactory { */ DataBuffer wrap(ByteBuffer byteBuffer); + /** + * Wrap the given {@code byte} array in a {@code DataBuffer}. + * @param bytes the byte array to wrap + * @return the wrapped buffer + */ + DataBuffer wrap(byte[] bytes); + } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java index 4862b45ae9..96d3a8303e 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java @@ -94,6 +94,12 @@ public class DefaultDataBufferFactory implements DataBufferFactory { return new DefaultDataBuffer(sliced, 0, byteBuffer.remaining(), this); } + @Override + public DataBuffer wrap(byte[] bytes) { + ByteBuffer wrapper = ByteBuffer.wrap(bytes); + return new DefaultDataBuffer(wrapper, 0, bytes.length, this); + } + @Override public String toString() { return "DefaultDataBufferFactory (preferDirect=" + this.preferDirect + ")"; diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java index 4373a6b62e..a88bd01ace 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java @@ -68,6 +68,12 @@ public class NettyDataBufferFactory implements DataBufferFactory { return new NettyDataBuffer(byteBuf, this); } + @Override + public DataBuffer wrap(byte[] bytes) { + ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); + return new NettyDataBuffer(byteBuf, this); + } + /** * Wrap the given Netty {@link ByteBuf} in a {@code NettyDataBuffer}. * @param byteBuf the Netty byte buffer to wrap diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java new file mode 100644 index 0000000000..0994605af7 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2016 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.core.codec; + +import java.util.Collections; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.tests.TestSubscriber; +import org.springframework.util.MimeTypeUtils; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Arjen Poutsma + */ +public class ByteArrayDecoderTests extends AbstractDataBufferAllocatingTestCase { + + private final ByteArrayDecoder decoder = new ByteArrayDecoder(); + + @Test + public void canDecode() { + assertTrue(this.decoder.canDecode(ResolvableType.forClass(byte[].class), + MimeTypeUtils.TEXT_PLAIN)); + assertFalse(this.decoder.canDecode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)); + assertTrue(this.decoder.canDecode(ResolvableType.forClass(byte[].class), + MimeTypeUtils.APPLICATION_JSON)); + } + + @Test + public void decode() { + DataBuffer fooBuffer = stringBuffer("foo"); + DataBuffer barBuffer = stringBuffer("bar"); + Flux source = Flux.just(fooBuffer, barBuffer); + Flux output = this.decoder.decode(source, + ResolvableType.forClassWithGenerics(Publisher.class, byte[].class), + null, Collections.emptyMap()); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValuesWith(bytes -> assertArrayEquals("foo".getBytes(), bytes), + bytes -> assertArrayEquals("bar".getBytes(), bytes)); + } + +} \ No newline at end of file diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java new file mode 100644 index 0000000000..c4f822ff46 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2016 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.core.codec; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.tests.TestSubscriber; +import org.springframework.util.MimeTypeUtils; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Arjen Poutsma + */ +public class ByteArrayEncoderTests extends AbstractDataBufferAllocatingTestCase { + + private ByteArrayEncoder encoder; + + @Before + public void createEncoder() { + this.encoder = new ByteArrayEncoder(); + } + + @Test + public void canEncode() { + assertTrue(this.encoder.canEncode(ResolvableType.forClass(byte[].class), + MimeTypeUtils.TEXT_PLAIN)); + assertFalse(this.encoder.canEncode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)); + assertTrue(this.encoder.canEncode(ResolvableType.forClass(byte[].class), + MimeTypeUtils.APPLICATION_JSON)); + } + + @Test + public void encode() { + byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + Flux source = Flux.just(fooBytes, barBytes); + + Flux output = this.encoder.encode(source, this.bufferFactory, + ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), + null, Collections.emptyMap()); + TestSubscriber + .subscribe(output) + .assertValuesWith(b -> { + byte[] buf = new byte[3]; + b.read(buf); + assertArrayEquals(fooBytes, buf); + }, b -> { + byte[] buf = new byte[3]; + b.read(buf); + assertArrayEquals(barBytes, buf); + }); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfigurationSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfigurationSupport.java index 174e800f73..28e7275895 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfigurationSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfigurationSupport.java @@ -28,6 +28,8 @@ import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; +import org.springframework.core.codec.ByteArrayDecoder; +import org.springframework.core.codec.ByteArrayEncoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.CharSequenceEncoder; @@ -307,6 +309,7 @@ public class WebReactiveConfigurationSupport implements ApplicationContextAware * {@link #configureMessageReaders(List)}. */ protected final void addDefaultHttpMessageReaders(List> readers) { + readers.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); readers.add(new DecoderHttpMessageReader<>(new StringDecoder())); readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder())); @@ -444,6 +447,7 @@ public class WebReactiveConfigurationSupport implements ApplicationContextAware */ protected final void addDefaultHttpMessageWriters(List> writers) { List> sseDataEncoders = new ArrayList<>(); + writers.add(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); writers.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); writers.add(new EncoderHttpMessageWriter<>(new CharSequenceEncoder())); writers.add(new ResourceHttpMessageWriter()); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultHandlerStrategiesBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultHandlerStrategiesBuilder.java index d67fec0c2f..699d6e7198 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultHandlerStrategiesBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultHandlerStrategiesBuilder.java @@ -23,6 +23,8 @@ import java.util.function.Supplier; import java.util.stream.Stream; import org.springframework.context.ApplicationContext; +import org.springframework.core.codec.ByteArrayDecoder; +import org.springframework.core.codec.ByteArrayEncoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.CharSequenceEncoder; @@ -65,8 +67,10 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { private final List viewResolvers = new ArrayList<>(); public void defaultConfiguration() { + messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); messageReader(new DecoderHttpMessageReader<>(new StringDecoder())); + messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); messageWriter(new EncoderHttpMessageWriter<>(new CharSequenceEncoder())); if (jaxb2Present) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 44200c184a..26269cd3a0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -32,6 +32,7 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.codec.ByteArrayDecoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.http.codec.DecoderHttpMessageReader; @@ -75,6 +76,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory public RequestMappingHandlerAdapter() { + this.messageReaders.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); this.messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); this.messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder())); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/DelegatingWebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/DelegatingWebReactiveConfigurationTests.java index 305e49fb75..ceb32d6de6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/DelegatingWebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/DelegatingWebReactiveConfigurationTests.java @@ -151,7 +151,7 @@ public class DelegatingWebReactiveConfigurationTests { verify(webReactiveConfigurer).addArgumentResolvers(any()); assertSame(formatterRegistry.getValue(), initializerConversionService); - assertEquals(5, readers.getValue().size()); + assertEquals(6, readers.getValue().size()); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationSupportTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationSupportTests.java index bd470960b3..a5d57aaab6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationSupportTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationSupportTests.java @@ -145,8 +145,9 @@ public class WebReactiveConfigurationSupportTests { assertNotNull(adapter); List> readers = adapter.getMessageReaders(); - assertEquals(5, readers.size()); + assertEquals(6, readers.size()); + assertHasMessageReader(readers, byte[].class, APPLICATION_OCTET_STREAM); assertHasMessageReader(readers, ByteBuffer.class, APPLICATION_OCTET_STREAM); assertHasMessageReader(readers, String.class, TEXT_PLAIN); assertHasMessageReader(readers, Resource.class, IMAGE_PNG); @@ -194,8 +195,9 @@ public class WebReactiveConfigurationSupportTests { assertEquals(0, handler.getOrder()); List> writers = handler.getMessageWriters(); - assertEquals(6, writers.size()); + assertEquals(7, writers.size()); + assertHasMessageWriter(writers, byte[].class, APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, ByteBuffer.class, APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, String.class, TEXT_PLAIN); assertHasMessageWriter(writers, Resource.class, IMAGE_PNG); @@ -219,8 +221,9 @@ public class WebReactiveConfigurationSupportTests { assertEquals(100, handler.getOrder()); List> writers = handler.getMessageWriters(); - assertEquals(6, writers.size()); + assertEquals(7, writers.size()); + assertHasMessageWriter(writers, byte[].class, APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, ByteBuffer.class, APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, String.class, TEXT_PLAIN); assertHasMessageWriter(writers, Resource.class, IMAGE_PNG); diff --git a/spring-web/src/main/java/org/springframework/web/client/reactive/DefaultWebClientStrategiesBuilder.java b/spring-web/src/main/java/org/springframework/web/client/reactive/DefaultWebClientStrategiesBuilder.java index 198af63493..b81f361a9a 100644 --- a/spring-web/src/main/java/org/springframework/web/client/reactive/DefaultWebClientStrategiesBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/client/reactive/DefaultWebClientStrategiesBuilder.java @@ -23,6 +23,8 @@ import java.util.function.Supplier; import java.util.stream.Stream; import org.springframework.context.ApplicationContext; +import org.springframework.core.codec.ByteArrayDecoder; +import org.springframework.core.codec.ByteArrayEncoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.CharSequenceEncoder; @@ -65,8 +67,10 @@ class DefaultWebClientStrategiesBuilder implements WebClientStrategies.Builder { public void defaultConfiguration() { + messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); messageReader(new DecoderHttpMessageReader<>(new StringDecoder(false))); + messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); messageWriter(new EncoderHttpMessageWriter<>(new CharSequenceEncoder())); if (jaxb2Present) {