From f8a21ab11bf1ecad7ac7866f280d42c70be48b8a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 27 Mar 2017 14:54:22 -0400 Subject: [PATCH] Add ClientCodecConfigurer Issue: SPR-15247 --- .../http/codec/ClientCodecConfigurer.java | 85 +++++++++++ .../codec/ClientCodecConfigurerTests.java | 135 ++++++++++++++++++ .../DefaultExchangeStrategiesBuilder.java | 68 +-------- 3 files changed, 224 insertions(+), 64 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java create mode 100644 spring-web/src/test/java/org/springframework/http/codec/ClientCodecConfigurerTests.java diff --git a/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java new file mode 100644 index 00000000000..d8df8615940 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java @@ -0,0 +1,85 @@ +/* + * 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; + +import java.util.List; + +import org.springframework.core.codec.Decoder; +import org.springframework.http.codec.json.Jackson2JsonDecoder; + +/** + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ClientCodecConfigurer extends AbstractCodecConfigurer { + + + public ClientCodecConfigurer() { + super(new ClientDefaultCodecConfigurer()); + } + + + @Override + public ClientDefaultCodecConfigurer defaultCodec() { + return (ClientDefaultCodecConfigurer) super.defaultCodec(); + } + + + @Override + protected void addDefaultTypedWriter(List> result) { + super.addDefaultTypedWriter(result); + defaultCodec().addWriterTo(result, FormHttpMessageWriter::new); + } + + @Override + protected void addDefaultObjectReaders(List> result) { + super.addDefaultObjectReaders(result); + defaultCodec().addServerSentEventReaderTo(result); + } + + + /** + * Extension of {@code DefaultCodecConfigurer} with extra client options. + */ + public static class ClientDefaultCodecConfigurer extends DefaultCodecConfigurer { + + /** + * Configure the {@code Decoder} to use for Server-Sent Events. + *

By default the {@link #jackson2Decoder} override is used for SSE. + * @param decoder the decoder to use + */ + public void serverSentEventDecoder(Decoder decoder) { + HttpMessageReader reader = new ServerSentEventHttpMessageReader(decoder); + getReaders().put(ServerSentEventHttpMessageReader.class, reader); + } + + + // Internal methods for building a list of default readers or writers... + + private void addServerSentEventReaderTo(List> result) { + addReaderTo(result, () -> findReader(ServerSentEventHttpMessageReader.class, () -> { + Decoder decoder = null; + if (jackson2Present) { + decoder = findDecoderReader( + Jackson2JsonDecoder.class, Jackson2JsonDecoder::new).getDecoder(); + } + return new ServerSentEventHttpMessageReader(decoder); + })); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/ClientCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/ClientCodecConfigurerTests.java new file mode 100644 index 00000000000..ac13471892d --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/ClientCodecConfigurerTests.java @@ -0,0 +1,135 @@ +/* + * 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; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +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; +import org.springframework.core.codec.DataBufferDecoder; +import org.springframework.core.codec.DataBufferEncoder; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.ResourceDecoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; +import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.util.MimeTypeUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; + +/** + * Unit tests for {@link ClientCodecConfigurer}. + * @author Rossen Stoyanchev + */ +public class ClientCodecConfigurerTests { + + private final ClientCodecConfigurer configurer = new ClientCodecConfigurer(); + + private final AtomicInteger index = new AtomicInteger(0); + + + @Test + public void defaultReaders() throws Exception { + List> readers = this.configurer.getReaders(); + assertEquals(9, readers.size()); + assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(ResourceDecoder.class, getNextDecoder(readers).getClass()); + assertStringDecoder(getNextDecoder(readers), true); + assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); + assertSseReader(readers); + assertStringDecoder(getNextDecoder(readers), false); + } + + @Test + public void defaultWriters() throws Exception { + List> writers = this.configurer.getWriters(); + assertEquals(9, writers.size()); + assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(ResourceHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); + assertStringEncoder(getNextEncoder(writers), true); + assertEquals(FormHttpMessageWriter.class, writers.get(this.index.getAndIncrement()).getClass()); + assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); + assertStringEncoder(getNextEncoder(writers), false); + } + + @Test + public void jackson2EncoderOverride() throws Exception { + + Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(); + this.configurer.defaultCodec().jackson2Decoder(decoder); + + assertSame(decoder, this.configurer.getReaders().stream() + .filter(reader -> ServerSentEventHttpMessageReader.class.equals(reader.getClass())) + .map(reader -> (ServerSentEventHttpMessageReader) reader) + .findFirst() + .map(ServerSentEventHttpMessageReader::getDecoder) + .filter(e -> e == decoder).orElse(null)); + } + + + private Decoder getNextDecoder(List> readers) { + HttpMessageReader reader = readers.get(this.index.getAndIncrement()); + assertEquals(DecoderHttpMessageReader.class, reader.getClass()); + return ((DecoderHttpMessageReader) reader).getDecoder(); + } + + private Encoder getNextEncoder(List> writers) { + HttpMessageWriter writer = writers.get(this.index.getAndIncrement()); + assertEquals(EncoderHttpMessageWriter.class, writer.getClass()); + return ((EncoderHttpMessageWriter) writer).getEncoder(); + } + + private void assertStringDecoder(Decoder decoder, boolean textOnly) { + assertEquals(StringDecoder.class, decoder.getClass()); + assertTrue(decoder.canDecode(forClass(String.class), MimeTypeUtils.TEXT_PLAIN)); + assertEquals(!textOnly, decoder.canDecode(forClass(String.class), MediaType.TEXT_EVENT_STREAM)); + } + + private void assertStringEncoder(Encoder encoder, boolean textOnly) { + assertEquals(CharSequenceEncoder.class, encoder.getClass()); + assertTrue(encoder.canEncode(forClass(String.class), MimeTypeUtils.TEXT_PLAIN)); + assertEquals(!textOnly, encoder.canEncode(forClass(String.class), MediaType.TEXT_EVENT_STREAM)); + } + + private void assertSseReader(List> readers) { + HttpMessageReader reader = readers.get(this.index.getAndIncrement()); + assertEquals(ServerSentEventHttpMessageReader.class, reader.getClass()); + Decoder decoder = ((ServerSentEventHttpMessageReader) reader).getDecoder(); + assertNotNull(decoder); + assertEquals(Jackson2JsonDecoder.class, decoder.getClass()); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java index 52af484ec25..de773b4bda8 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultExchangeStrategiesBuilder.java @@ -23,27 +23,14 @@ 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; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.StringDecoder; +import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.EncoderHttpMessageWriter; -import org.springframework.http.codec.FormHttpMessageWriter; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; -import org.springframework.http.codec.ResourceHttpMessageWriter; -import org.springframework.http.codec.ServerSentEventHttpMessageReader; -import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.http.codec.json.Jackson2JsonEncoder; -import org.springframework.http.codec.xml.Jaxb2XmlDecoder; -import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Default implementation of {@link ExchangeStrategies.Builder}. @@ -53,62 +40,15 @@ import org.springframework.util.ClassUtils; */ class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Builder { - private static final boolean jackson2Present = - ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", - DefaultExchangeStrategiesBuilder.class.getClassLoader()) && - ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", - DefaultExchangeStrategiesBuilder.class.getClassLoader()); - - private static final boolean jaxb2Present = - ClassUtils.isPresent("javax.xml.bind.Binder", - DefaultExchangeStrategiesBuilder.class.getClassLoader()); - - private final List> messageReaders = new ArrayList<>(); private final List> messageWriters = new ArrayList<>(); public void defaultConfiguration() { - defaultReaders(); - defaultWriters(); - } - - private void defaultReaders() { - messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); - messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); - messageReader(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly(false))); - if (jaxb2Present) { - messageReader(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); - } - if (jackson2Present) { - messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); - } - messageReader(new ServerSentEventHttpMessageReader(getSseDecoder())); - messageReader(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(false))); - } - - private Decoder getSseDecoder() { - if (jackson2Present) { - return new Jackson2JsonDecoder(); - } - else { - return null; - } - } - - private void defaultWriters() { - messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); - messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); - messageWriter(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); - messageWriter(new ResourceHttpMessageWriter()); - messageWriter(new FormHttpMessageWriter()); - if (jaxb2Present) { - messageWriter(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); - } - if (jackson2Present) { - messageWriter(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder())); - } + ClientCodecConfigurer configurer = new ClientCodecConfigurer(); + configurer.getReaders().forEach(this::messageReader); + configurer.getWriters().forEach(this::messageWriter); } public void applicationContext(ApplicationContext applicationContext) {